Controllers Overview
Custom controllers (Active Classes) in Magentrix enable developers to build powerful server-side logic for handling HTTP requests, processing data, and delivering dynamic responses. Built on C# .NET Framework 4.8, controllers serve as the backbone of custom web applications and APIs within the Magentrix platform.
Table of Contents
- What Are Custom Controllers?
- Two Types of Controllers
- URL Structure Comparison
- Controller Architecture
- AspxController Base Class
- Creating Page Controllers (with Active Page)
- Creating Standalone Controllers (without Active Page)
- Action Methods
- Passing Models to Views (Page Controllers Only)
- When to Use Page vs Standalone Controllers
- When to Use Controllers vs Magentrix REST API
- Common Controller Patterns
- Controller Security and Authentication
- Next Steps
- Quick Reference
- Key Takeaways
- Practical Examples Summary
- Common Pitfalls and Solutions
- Debugging Tips
- Architecture Decision Guide
- Performance Considerations
- Security Best Practices
- Summary
What Are Custom Controllers?
Custom controllers are C# classes that inherit from AspxController and handle incoming HTTP requests. Controllers execute business logic, interact with the database, and return appropriate responses such as rendered pages, JSON data, files, or redirects.
Key Characteristics:
- Written in C# (.NET Framework 4.8)
- Inherit from the
AspxController base class - Handle HTTP GET and POST requests
- Support multiple response types (views, JSON, files, redirects, PDFs, etc.)
- Integrate seamlessly with Magentrix database entities
- Execute within the Magentrix MVC (Model-View-Controller) architecture
- Can work with or without Active Pages
Two Types of Controllers
Magentrix supports two distinct controller patterns, each with different URL routing and use cases:
1. Page Controllers (with Active Page)
Purpose: Render user-facing web pages using Active Page templates.
URL Pattern:
/aspx/{PageName}/{ActionName}?{parameters}
Requirements:
- Must have a corresponding Active Page with the same name
- Controller class name must match:
{PageName}Controller
Use Cases:
- Interactive web forms and dashboards
- Multi-step wizards
- User-facing CRUD interfaces
- Any scenario requiring server-rendered HTML with Magentrix UI components
Example:
// Controller: ContactManagerController
// Active Page: ContactManager
// URL: /aspx/ContactManager/Edit?id=003R000000haXk3IAE
public class ContactManagerController : AspxController
{
public override ActionResponse Index()
{
return View(); // Renders the ContactManager Active Page
}
public ActionResponse Retrieve(string id)
{
Contact contact = Database.Retrieve<Contact>(id);
return View(contact);
}
}
2. Standalone Controllers (without Active Page)
Purpose: Provide programmatic endpoints for APIs, webhooks, integrations, and background processing.
URL Pattern:
/acls/{ControllerName}/{ActionName}?{parameters}
Requirements:
- No Active Page required
- Controller class name can be anything (no strict naming convention)
- Must explicitly specify the action name in the URL
Use Cases:
- Custom REST APIs that return JSON
- Webhook endpoints for external system integrations
- File generation and download services
- Background processing tasks (Scheduled Jobs)
- Programmatic data access from external applications
- Internal microservices or utility endpoints
Example:
// Controller: ContactApiController (any name works)
// No Active Page needed
// URL: /acls/ContactApi/GetContact?id=003R000000haXk3IAE
public class ContactApiController : AspxController
{
[HandleExceptionsForJson]
public ActionResponse GetContact(string id)
{
var contact = Database.Retrieve<Contact>(id);
return Json(contact, JsonRequestBehavior.AllowGet);
}
[HttpPost]
[HandleExceptionsForJson]
public ActionResponse CreateContact(Contact model)
{
if (ModelState.IsValid)
{
Database.Insert(model);
return Json(new { success = true, id = model.Id });
}
return Json(new { success = false, errors = ModelState });
}
}
URL Structure Comparison
| Controller Type | URL Pattern | Active Page Required | Typical Use |
|---|
| Page Controller | /aspx/{PageName}/{Action} | ✅ Yes | Web pages, forms, dashboards |
| Standalone Controller | /acls/{ControllerName}/{Action} | ❌ No | APIs, webhooks, batch jobs |
💡 Note: Both controller types inherit from AspxController and have identical access to database operations, user information, security, and all response types.
Controller Architecture
Request Lifecycle
When a user or system accesses a controller endpoint, the following sequence occurs:
- Request Routing: Magentrix receives the HTTP request and parses the URL to determine the controller type (
/aspx/ or /acls/), controller name, and action - Controller Instantiation: The platform instantiates the controller class
- Authentication & Authorization: The system validates user authentication and checks security attributes
- Action Execution: The specified action method is invoked with parameters from the URL or request body
- Business Logic: The action executes custom logic (database queries, validation, calculations, etc.)
- Response Generation: The action returns an
ActionResponse object (View, JSON, Redirect, File, etc.) - Response Rendering: Magentrix processes the response and delivers the final output to the requester
URL Components
Page Controller URL:
/aspx/{PageName}/{ActionName}?{parameters}
Standalone Controller URL:
/acls/{ControllerName}/{ActionName}?{parameters}
Components:
aspx: Area identifier for Active Page controllersacls: Area identifier for standalone Active Class controllersPageName/ControllerName: The name of your controllerActionName: The action method to invoke
- Optional for
/aspx/ URLs (defaults to Index) - Optional for
/acls/ URLs (defaults to Index)
parameters: Optional query string or POST body parameters
Examples:
Page Controller:
/aspx/ContactManager/Edit?id=003R000000haXk3IAE
- Controller:
ContactManagerController - Action:
Edit - Parameter:
id = 003R000000haXk3IAE
Standalone Controller:
/acls/ContactApi/GetContact?id=003R000000haXk3IAE
- Controller:
ContactApiController - Action:
GetContact - Parameter:
id = 003R000000haXk3IAE
AspxController Base Class
All custom controllers inherit from AspxController, which provides comprehensive functionality regardless of whether an Active Page exists.
Core Functionality:
- Database access through the
Database property - User information via
UserInfo and SystemInfo - Session and cookie management
- Request and response utilities
- Security and authorization helpers
- Message display capabilities (errors, warnings, info)
- Model binding and validation
Response Type Factories:
View()- Render Active Pages (Page Controllers only)PartialView() - Render page fragments (Page Controllers only)Json() - Return JSON dataRedirect()- Navigate to different URLsFile() - Stream file downloadsPdf() - Generate dynamic PDFsContent() - Return raw text/HTML/XML/CSS/JavaScriptStorageFile() - Stream files from Magentrix Cloud Storage- And more (covered in the Controller Responses section)
Creating Page Controllers (with Active Page)
Page controllers render user-facing web pages using Active Page templates.
Naming Convention
Controller class names must follow this strict pattern for Page Controllers:
{PageName}Controller
Example:
- Active Page Name:
ContactManager - Controller Class Name:
ContactManagerController - URL:
/aspx/ContactManager
⚠ Critical: The controller name must exactly match your Active Page name with "Controller" appended. Active Page templates must also be connected to the controllers.
Basic Page Controller Structure
public class ContactManagerController : AspxController
{
// Default action - handles GET requests to /aspx/ContactManager
public override ActionResponse Index()
{
return View();
}
}
Page Controller with Model
public class ContactManagerController : AspxController
{
public override ActionResponse Index()
{
// Retrieve a contact from the database
var contact = Database.Retrieve<Contact>("003R000000haXk3IAE");
// Pass the model to the view
return View(contact);
}
}
Page Controller with GET and POST Actions
public class ContactManagerController : AspxController
{
// GET: Initial page load
public override ActionResponse Index()
{
var model = new Contact();
return View(model);
}
// POST: Form submission
[HttpPost]
public ActionResponse Index(Contact model)
{
if (ModelState.IsValid)
{
Database.Insert(model);
AspxPage.AddMessage("Contact saved successfully.");
return RedirectToAction("Index");
}
return View(model);
}
}
Creating Standalone Controllers (without Active Page)
Standalone controllers provide programmatic endpoints without requiring Active Page templates.
Naming Convention
Standalone controllers can use any naming convention:
// All of these are valid:
public class ContactApiController : AspxController { }
public class WebhookHandlerController : AspxController { }
public class DataExportController : AspxController { }
public class ScheduledTaskController : AspxController { }
💡 Best Practice: Use descriptive names that clearly indicate the controller's purpose (e.g., ContactApiController, OrderWebhookController).
Basic Standalone Controller
// URL: /acls/ContactApi/GetAll
public class ContactApiController : AspxController
{
[HandleExceptionsForJson]
public ActionResponse GetAll()
{
var contacts = Database.Query<Contact>()
.OrderBy(c => c.LastName)
.ToList();
return Json(contacts, JsonRequestBehavior.AllowGet);
}
}
REST API Controller
public class ContactApiController : AspxController
{
// GET: /acls/ContactApi/Get?id=003R000000haXk3IAE
[HandleExceptionsForJson]
public ActionResponse Get(string id)
{
var contact = Database.Retrieve<Contact>(id);
return Json(contact, JsonRequestBehavior.AllowGet);
}
// POST: /acls/ContactApi/Create
[HttpPost]
[HandleExceptionsForJson]
public ActionResponse Create(Contact model)
{
if (ModelState.IsValid)
{
Database.Insert(model);
return Json(new { success = true, id = model.Id });
}
return Json(new { success = false, errors = ModelState });
}
// POST: /acls/ContactApi/Update
[HttpPost]
[HandleExceptionsForJson]
public ActionResponse Update(Contact model)
{
if (ModelState.IsValid)
{
Database.Update(model);
return Json(new { success = true });
}
return Json(new { success = false, errors = ModelState });
}
// POST: /acls/ContactApi/Delete?id=003R000000haXk3IAE
[HttpPost]
[HandleExceptionsForJson]
public ActionResponse Delete(string id)
{
var contact = Database.Retrieve(id);
if (contact != null)
{
Database.Delete(contact);
return Json(new { success = true });
}
return Json(new { success = false });
}
}
File Generation Controller
// URL: /acls/ReportGenerator/ExportContacts
public class ReportGeneratorController : AspxController
{
public ActionResponse ExportContacts()
{
var contacts = Database.Query<Contact>().ToList();
// Generate CSV content
var csv = new StringBuilder();
csv.AppendLine("First Name,Last Name,Email");
foreach (var contact in contacts)
csv.AppendLine($"{contact.FirstName},{contact.LastName},{contact.Email}");
var fileBytes = Encoding.UTF8.GetBytes(csv.ToString());
return File(fileBytes, "text/csv", "contacts.csv");
}
public ActionResponse GeneratePdf(string accountId)
{
var account = Database.Retrieve(accountId);
return Pdf(account, $"Account_{account.Name}.pdf");
}
}
Webhook Controller
// URL: /acls/Webhooks/ProcessOrder
public class WebhooksController : AspxController
{
[HttpPost]
[HandleExceptionsForJson]
public ActionResponse ProcessOrder()
{
// Read raw POST body
var jsonPayload = Request.GetBodyAsString();
// Parse JSON using Magentrix JsonHelper
var orderData = JsonHelper.FromJson<OrderWebhookData>(jsonPayload);
// Process the order
var order = new Order
{
ExternalId = orderData.OrderId,
Amount = orderData.Total,
Status = "Processing"
};
Database.Insert(order);
return Json(new { success = true, orderId = order.Id });
}
}
Scheduled Job Controller
// Scheduled Tasks Controller
// URL Pattern: /acls/ScheduledTasks/{ActionName}
//
// IMPORTANT: All scheduled task actions MUST be decorated with [HttpPost]
// Magentrix Scheduled Jobs will call these endpoints via HTTP POST requests.
// Configure scheduled jobs in Setup > Automation > Scheduled Jobs
public class ScheduledTasksController : AspxController
{
// Daily cleanup task - removes temporary data older than 30 days
// Schedule: Daily at 2:00 AM
// URL: /acls/ScheduledTasks/DailyCleanup
[HttpPost]
public ActionResponse DailyCleanup()
{
// Delete old records
var oldRecords = Database.Query<TempData>()
.Where(t => t.CreatedDate < DateTime.Now.AddDays(-30))
.ToList();
Database.Delete(oldRecords);
return Content($"Cleanup complete. Deleted {oldRecords.Count} records.");
}
// Weekly report generation task
// Schedule: Weekly on Monday at 8:00 AM
// URL: /acls/ScheduledTasks/SendWeeklyReport
[HttpPost]
public ActionResponse SendWeeklyReport()
{
// Generate and email weekly reports
var accounts = Database.Query<Account>()
.Where(a => a.Type == "Customer")
.ToList();
// Email logic here
// ...
return Content($"Weekly report sent for {accounts.Count} accounts.");
}
}
Action Methods
What Are Actions?
Actions are public methods in your controller that respond to HTTP requests. Each action must return an ActionResponse object.
Rules:
- All public methods are considered actions
- Action names must be unique (no overloads except for different HTTP verbs)
- The
Index() action is special:
- For Page Controllers (
/aspx/): It's the default action and cannot accept parameters - For Standalone Controllers (
/acls/): It must be explicitly called in the URL
- Actions can accept parameters from the URL query string or request body
Action Examples
Simple Action:
public ActionResponse HelloWorld()
{
return Content("Hello, World!");
}
Page Controller URL: /aspx/YourPage/HelloWorld
Standalone Controller URL: /acls/YourController/HelloWorld
Action with Parameters:
public ActionResponse GetContact(string id)
{
var contact = Database.Retrieve(id);
return Json(contact, JsonRequestBehavior.AllowGet);
}
URL: /acls/ContactApi/GetContact?id=003R000000haXk3IAE
Multiple Parameters:
public ActionResponse Search(string firstName, string lastName, string email)
{
var contacts = Database.Query<Contact>()
.Where(c => c.FirstName.Contains(firstName) ||
c.LastName.Contains(lastName) ||
c.Email.Contains(email))
.ToList();
return Json(contacts, JsonRequestBehavior.AllowGet);
}
URL: /acls/ContactApi/Search?firstName=John&lastName=Doe&email=example.com
POST Action with Model Binding:
[HttpPost]
[HandleExceptionsForJson]
public ActionResponse CreateAccount(Account model)
{
if (ModelState.IsValid)
{
Database.Insert(model);
return Json(new { success = true, id = model.Id });
}
return Json(new { success = false, errors = ModelState });
}
Passing Models to Views (Page Controllers Only)
Page controllers pass data to Active Pages using models. A model can be any C# object - typically a database entity or a custom class.
Simple Model Binding
Controller:
public class ContactDetailsController : AspxController
{
public override ActionResponse Index()
{
var contact = new Contact
{
FirstName = "John",
LastName = "Doe",
Email = "john.doe@example.com"
};
return View(contact);
}
}
Active Page (ASPX markup):
<aspx:AspxPage runat='server' title='Contact Details'>
<body>
<aspx:ViewPanel runat='server' title='Contact Information'>
<aspx:ViewSection runat='server' title='Details' columns='one'>
<aspx:InputField runat='server' value='{!Model.FirstName}' />
<aspx:InputField runat='server' value='{!Model.LastName}' />
<aspx:InputField runat='server' value='{!Model.Email}' />
</aspx:ViewSection>
</aspx:ViewPanel>
</body>
</aspx:AspxPage>
The {!Model.PropertyName} syntax binds data from the controller's model to the page.
Collection Models
Controller:
public ActionResponse Index()
{
var accounts = Database.Query<Account>()
.Where(a => a.Type == "Partner")
.OrderBy(a => a.Name)
.ToList();
return View(accounts);
}
Active Page:
<aspx:Repeater runat='server' value='{!Model}' var='account'>
<body>
<table class='data-table'>
<thead>
<tr>
<th>Account Name</th>
<th>Type</th>
<th>Industry</th>
</tr>
</thead>
<tbody>
<tr class='record-row'>
<td><aspx:Field runat='server' value='{!account.Name}'/></td>
<td><aspx:Field runat='server' value='{!account.Type}'/></td>
<td><aspx:Field runat='server' value='{!account.Industry}'/></td>
</tr>
</tbody>
</table>
</body>
</aspx:Repeater>
When to Use Page vs Standalone Controllers
Use Page Controllers (/aspx/) When:
✅ Building interactive user interfaces - Forms, dashboards, and multi-step workflows
✅ You need server-side rendering with Active Pages - Leverage Magentrix UI components like ViewPanels, InputFields, Repeaters
✅ Creating wizard-style or multi-page experiences - User navigates through sequential steps
✅ You need session-based state management - Maintain data across multiple page loads
✅ Building administrative interfaces - Configuration pages, settings screens
✅ Working with complex page layouts - ViewSections, related lists, custom layouts
Use Standalone Controllers (/acls/) When:
✅ Building REST APIs - Return JSON for programmatic access
✅ Creating webhook endpoints - Receive data from external systems (payment processors, CRMs, etc.)
✅ Implementing scheduled jobs - Background tasks executed by Magentrix Scheduled Jobs
✅ Generating files for download - CSV exports, PDF reports, Excel files
✅ Building internal microservices - Utility endpoints called by other parts of the system
✅ Creating AJAX endpoints - JavaScript on Active Pages can call /acls/ endpoints
✅ External system integrations - Allow third-party applications to interact with your Magentrix portal
✅ Stateless operations - Each request is independent without session state
When to Use Controllers vs Magentrix REST API
Magentrix provides both custom controllers and a comprehensive built-in REST API. Understanding when to use each approach is critical for effective development.
Use Custom Controllers When:
✅ You need custom business logic - Complex validation, calculations, or workflows not covered by standard APIs
✅ Building custom user interfaces - Interactive web pages with Active Pages
✅ Complex database queries - Multi-entity joins, aggregations, or custom filtering
✅ File generation - Dynamic PDFs, CSV exports, or custom reports
✅ Implementing custom authentication flows - Login pages, password reset, SSO integration
✅ Background processing - Scheduled jobs, batch operations, data synchronization
✅ Webhook handling - Process incoming data from external systems
✅ Custom authorization logic - Beyond standard security role permissions
Use Magentrix REST API When:
✅ Performing standard CRUD operations - Create, read, update, delete on entities
✅ Querying entity data - Retrieve records with standard filters and sorting
✅ Integrating with external systems - Third-party applications need data access
✅ Mobile app development - Native iOS/Android apps consuming portal data
✅ JavaScript/AJAX calls - Frontend applications making API calls
✅ Standard operations are sufficient - No custom logic required
✅ You need OAuth authentication - REST API supports token-based authentication
⚠
Note: Magentrix provides a full suite of built-in REST APIs for most common operations. Always check the
Magentrix REST API documentation before building custom controllers for standard CRUD operations.
Common Controller Patterns
CRUD Operations Pattern (Page Controller)
public class AccountManagerController : AspxController
{
// List all accounts (Read)
public override ActionResponse Index()
{
var accounts = Database.Query<Account>()
.OrderBy(a => a.Name)
.ToList();
return View(accounts);
}
// Display form to create new account (Create - GET)
public ActionResponse New()
{
return View(new Account());
}
// Save new account (Create - POST)
[HttpPost]
public ActionResponse New(Account model)
{
if (ModelState.IsValid)
{
Database.Insert(model);
AspxPage.AddMessage("Account created successfully.");
return RedirectToAction("Index");
}
AspxPage.AddError("Please correct the errors below.");
return View(model);
}
// Display form to edit existing account (Update - GET)
public ActionResponse Edit(string id)
{
Account account = Database.Retrieve(id);
return View(account);
}
// Save edited account (Update - POST)
[HttpPost]
public ActionResponse Edit(Account model)
{
if (ModelState.IsValid)
{
Database.Update(model);
AspxPage.AddMessage("Account updated successfully.");
return RedirectToAction("Index");
}
AspxPage.AddError("Please correct the errors below.");
return View(model);
}
// Delete account (Delete - POST)
[HttpPost]
public ActionResponse Delete(string id)
{
var account = Database.Retrieve(id);
if (account != null)
Database.Delete(account);
AspxPage.AddMessage("Account deleted successfully.");
return RedirectToAction("Index");
}
}
REST API Pattern (Standalone Controller)
public class AccountApiController : AspxController
{
// GET: /acls/AccountApi/List
[HandleExceptionsForJson]
public ActionResponse List()
{
var accounts = Database.Query<Account>()
.OrderBy(a => a.Name)
.ToList();
return Json(accounts, JsonRequestBehavior.AllowGet);
}
// GET: /acls/AccountApi/Get?id=001R000000haXk3IAE
[HandleExceptionsForJson]
public ActionResponse Get(string id)
{
var account = Database.Retrieve(id);
return Json(account, JsonRequestBehavior.AllowGet);
}
// POST: /acls/AccountApi/Create
[HttpPost]
[HandleExceptionsForJson]
public ActionResponse Create(Account model)
{
if (ModelState.IsValid)
{
Database.Insert(model);
return Json(new { success = true, id = model.Id });
}
return Json(new { success = false, errors = ModelState });
}
// POST: /acls/AccountApi/Update
[HttpPost]
[HandleExceptionsForJson]
public ActionResponse Update(Account model)
{
if (ModelState.IsValid)
{
Database.Update(model);
return Json(new { success = true });
}
return Json(new { success = false, errors = ModelState });
}
// POST: /acls/AccountApi/Delete?id=001R000000haXk3IAE
[HttpPost]
[HandleExceptionsForJson]
public ActionResponse Delete(string id)
{
Database.Delete<Account>(id);
return Json(new { success = true });
}
}
Search and Filter Pattern
public ActionResponse Search(string searchTerm, string type, string industry)
{
var query = Database.Query<Account>();
// Apply search term filter
if (!string.IsNullOrEmpty(searchTerm))
query = query.Where(a => a.Name.Contains(searchTerm) ||
a.Description.Contains(searchTerm));
// Apply type filter
if (!string.IsNullOrEmpty(type))
query = query.Where(a => a.Type == type);
// Apply industry filter
if (!string.IsNullOrEmpty(industry))
query = query.Where(a => a.Industry == industry);
var results = query.OrderBy(a => a.Name).ToList();
// Return View for Page Controller
return View(results);
// OR return JSON for Standalone Controller
// return Json(results, JsonRequestBehavior.AllowGet);
}
Master-Detail Pattern (Page Controller)
public ActionResponse ViewAccount(string id)
{
// Retrieve master record
var account = Database.Retrieve<Account>(id);
// Retrieve related detail records
var contacts = Database.Query<Contact>()
.Where(c => c.AccountId == id)
.OrderBy(c => c.LastName)
.ToList();
var opportunities = Database.Query<Opportunity>()
.Where(o => o.AccountId == id)
.OrderByDescending(o => o.CloseDate)
.ToList();
// Pass data to a custom view model
var viewModel = new AccountDetailsViewModel
{
Account = account,
Contacts = contacts,
Opportunities = opportunities
};
return View(viewModel);
}
File Export Pattern (Standalone Controller)
public class ExportController : AspxController
{
// GET: /acls/Export/ContactsCsv
public ActionResponse ContactsCsv()
{
var contacts = Database.Query<Contact>().ToList();
var csv = new StringBuilder();
csv.AppendLine("First Name,Last Name,Email,Phone");
foreach (var contact in contacts)
csv.AppendLine($"{contact.FirstName},{contact.LastName},{contact.Email},{contact.Phone}");
var fileBytes = Encoding.UTF8.GetBytes(csv.ToString());
return File(fileBytes, "text/csv", "contacts.csv");
}
// GET: /acls/Export/AccountsPdf
public ActionResponse AccountsPdf()
{
var accounts = Database.Query<Account>()
.OrderBy(a => a.Name)
.ToList();
return Pdf(accounts, "accounts.pdf");
}
}
Thank you for the corrections! Here's the updated version:
Controller Security and Authentication
By default, controllers require authentication. However, controller access is determined by:
- Security Role Assignment - The Active Page or Active Class must be assigned to specific Security Roles in Setup
- Guest Role Assignment - If assigned to the Guest role, no authentication is required
Security Role Assignment
Controllers are only accessible to users whose Security Role has been granted access to the controller's Active Page or Active Class.
Configuration: Setup > Security > Security Roles > [Role Name] > Active Pages / Classes
// This controller is accessible ONLY if the user's Security Role
// has been assigned access to this Active Page or Active Class in Setup
public class ContactManagerController : AspxController
{
public override ActionResponse Index()
{
return View();
}
}
Guest Role Access (Public Controllers)
If you assign the Active Page or Active Class to the Guest Security Role, the controller becomes publicly accessible without authentication.
// Public controller - accessible without login
// Must be assigned to Guest role in Setup > Security Roles > Active Pages / Classes
public class PublicContactFormController : AspxController
{
public override ActionResponse Index()
{
return View(new ContactForm());
}
[HttpPost]
[CaptchaValidator]
public ActionResponse Index(ContactForm model)
{
if (ModelState.IsValid)
{
Database.Insert(model);
AspxPage.AddMessage("Thank you for contacting us!");
return RedirectToAction("ThankYou");
}
return View(model);
}
}
Additional Authorization Checks
Even when a controller is accessible via role assignment, you can add additional authorization logic:
Check User Authentication:
public ActionResponse MyAction()
{
// Additional check: ensure user is logged in
if (SystemInfo.IsGuestUser)
return UnauthorizedAccessResponse();
// Execute action logic
return View();
}
Entity Permission Authorization:
[AuthorizeAction(Entity = "Contact", Action = StandardAction.Edit)]
public ActionResponse EditContact(string id)
{
// User must have Edit permission on Contact entity
var contact = Database.Retrieve<Contact>(id);
return View(contact);
}
Role-Based Authorization:
[Authorize(Roles = "Administrator,Sales Manager")]
public ActionResponse AdminDashboard()
{
// User must have Administrator OR Sales Manager role
// AND their Security Role must be assigned this Active Page or Active Class
return View();
}
Security Hierarchy
Magentrix security works in layers:
1. Active Page or Active Class Assignment to Security Role (Setup)
↓ (User's role must be assigned the controller)
2. [Authorize] Attribute (optional)
↓ (Check for specific role names)
3. [AuthorizeAction] Attribute (optional)
↓ (Check entity-level permissions)
4. Custom Authorization Logic (optional)
↓ (Business rules, account isolation, etc.)
💡
Important:- Standalone controllers (
/acls/) follow the same security model - External systems calling these endpoints must authenticate using Magentrix credentials or API tokens
- Always assign Active Pages or Active Classes to appropriate Security Roles in Setup before users can access them
📘 See Controller Security & Authorization for complete details on implementing multi-layered security.
Next Steps
Now that you understand the fundamentals of custom controllers, explore these related topics:
- Working with Controllers - Deep dive into action methods, parameters, cookies, user information, and utilities
- Controller Responses - Master all response types (View, Redirect, JSON, File, PDF, Content, etc.)
- Controller Security & Authorization - Implement role-based access control and entity permissions
- Controller Attributes - Use annotations to control request handling, serialization, and behavior
- Advanced Controller Patterns - Best practices, error handling, and complex scenarios
Quick Reference
URL Patterns
| Controller Type | URL Pattern | Example |
|---|
| Page Controller | /aspx/{PageName}/{Action} | /aspx/ContactManager/Edit?id=123 |
| Standalone Controller | /acls/{ControllerName}/{Action} | /acls/ContactApi/GetContact?id=123 |
Essential Controller Methods
| Method | Purpose |
|---|
View() | Render an Active Page (Page Controllers only) |
View(model) | Render a page with data |
Redirect(url) | Navigate to a different URL |
RedirectToAction(action) | Navigate to another action |
Json(data) | Return JSON for APIs |
File(bytes, contentType, filename) | Download files |
Pdf(model, filename) | Generate PDFs |
Content(text) | Return plain text/HTML/XML |
StorageFile(id, contentType, filename) | Stream files from cloud storage |
Essential Properties
| Property | Purpose |
|---|
Database | Access database operations (Query, Retrieve, Insert, Update, Delete) |
UserInfo | Current user information (Name, Email, IsPortalUser, Account, Contact) |
SystemInfo | System and portal information (IsGuestUser, PortalUrl, etc.) |
AspxPage | Page utilities (messages, cookies, parameters) |
ModelState | Form validation state |
DataBag | Temporary data storage across requests |
Common Attributes
| Attribute | Purpose |
|---|
[HttpPost] | Restrict action to POST requests only |
[AuthorizeAction] | Require specific entity permissions |
[Authorize] | Require specific security roles |
[HandleExceptionsForJson] | Return errors as JSON (for APIs) |
[SerializeViewData] | Preserve model in ViewState across requests |
Key Takeaways
✅ Two controller types: Page Controllers (/aspx/) with Active Pages, and Standalone Controllers (/acls/) without Active Pages
✅ Both inherit from AspxController: Same base functionality, database access, security, and response types
✅ Page Controllers are for interactive web UIs; **Standalone Controllers** are for APIs, webhooks, and background jobs
✅ URL routing differs: /aspx/{PageName}/{Action} vs /acls/{ControllerName}/{Action}
✅ Standalone controllers require explicit action names in the URL (no default Index route)
✅ Authentication is always required: Both controller types enforce Magentrix authentication
✅ All response types are available: JSON, File, PDF, Content, Redirect—regardless of controller type
✅ Use the right tool: Custom controllers for complex logic, Magentrix REST API for standard CRUD operations
Practical Examples Summary
Example 1: Simple Page Controller
Use Case: Display a contact form
// File: ContactFormController.cs
// Active Page: ContactForm
// URL: /aspx/ContactForm
public class ContactFormController : AspxController
{
public override ActionResponse Index()
{
return View(new Contact());
}
[HttpPost]
public ActionResponse Index(Contact model)
{
if (ModelState.IsValid)
{
Database.Insert(model);
AspxPage.AddMessage("Contact created successfully!");
return RedirectToAction("Index");
}
return View(model);
}
}
Example 2: REST API Controller
Use Case: Provide JSON API for external systems
// File: ContactApiController.cs
// No Active Page needed
// URL: /acls/ContactApi/GetAll
public class ContactApiController : AspxController
{
[HandleExceptionsForJson]
public ActionResponse GetAll()
{
var contacts = Database.Query<Contact>().ToList();
return Json(contacts, JsonRequestBehavior.AllowGet);
}
[HttpPost]
[HandleExceptionsForJson]
public ActionResponse Create(Contact model)
{
Database.Insert(model);
return Json(new { success = true, id = model.Id });
}
}
Example 3: File Export Controller
Use Case: Generate downloadable reports
// File: ReportsController.cs
// No Active Page needed
// URL: /acls/Reports/ExportContacts
public class ReportsController : AspxController
{
public ActionResponse ExportContacts()
{
var contacts = Database.Query<Contact>()
.OrderBy(c => c.LastName)
.ToList();
var csv = new StringBuilder();
csv.AppendLine("First Name,Last Name,Email");
foreach (var contact in contacts)
csv.AppendLine($"{contact.FirstName},{contact.LastName},{contact.Email}");
var bytes = Encoding.UTF8.GetBytes(csv.ToString());
return File(bytes, "text/csv", "contacts.csv");
}
public ActionResponse GeneratePdf(string accountId)
{
var account = Database.Retrieve<Account>(accountId);
return Pdf(account, $"Account_{account.Name}.pdf");
}
}
Example 4: Webhook Handler
Use Case: Receive data from external payment processor
// File: WebhookController.cs
// No Active Page needed
// URL: /acls/Webhook/PaymentReceived
public class WebhookController : AspxController
{
[HttpPost]
[HandleExceptionsForJson]
public ActionResponse PaymentReceived()
{
// Read raw POST body using Magentrix method
var json = Request.GetBodyAsString();
// Parse JSON using Magentrix JsonHelper
var payment = JsonHelper.FromJson<PaymentData>(json);
// Find related opportunity
var opportunity = Database.Query<Opportunity>()
.Where(o => o.ExternalPaymentId == payment.TransactionId)
.FirstOrDefault();
if (opportunity != null)
{
opportunity.Stage = "Closed Won";
opportunity.Amount = payment.Amount;
Database.Update(opportunity);
}
return Json(new { success = true });
}
}
Example 5: Scheduled Job Controller
Use Case: Background task executed by Magentrix Scheduled Jobs
// File: MaintenanceController.cs
// No Active Page needed
// URL: /acls/Maintenance/CleanupOldRecords
// Called by Magentrix Scheduled Job (e.g., daily at 2 AM)
public class MaintenanceController : AspxController
{
[HttpPost]
public ActionResponse CleanupOldRecords()
{
var cutoffDate = DateTime.Now.AddDays(-90);
// Delete old temporary records
var oldRecords = Database.Query<BusinessLog__c>()
.Where(t => t.CreatedDate < cutoffDate)
.ToList();
Database.Delete(oldRecords);
// Log results
var message = $"Cleanup complete. Deleted {oldRecords.Count} records.";
// Return plain text response
return Content(message);
}
[HttpPost]
public ActionResponse SendWeeklyDigest()
{
// Get users who should receive digest
var users = Database.Query<Contact>()
.Where(c => c.ReceiveWeeklyDigest == true)
.ToList();
foreach (var user in users)
{
// Generate and send email digest
// Email logic here...
}
return Content($"Weekly digest sent to {users.Count} users.");
}
}
Example 6: Mixed Controller (Both Page and API Actions)
Use Case: Dashboard page with AJAX endpoints
// File: DashboardController.cs
// Active Page: Dashboard
// Page URL: /aspx/Dashboard
// API URLs: /acls/Dashboard/GetStats, /acls/Dashboard/RefreshData
public class DashboardController : AspxController
{
// Page action - renders the dashboard page
public override ActionResponse Index()
{
return View();
}
// API action - returns JSON for AJAX calls
[HandleExceptionsForJson]
public ActionResponse GetStats()
{
var stats = new
{
TotalAccounts = Database.Query<Account>().Count(),
TotalContacts = Database.Query<Contact>().Count(),
OpenOpportunities = Database.Query<Opportunity>()
.Where(o => o.IsClosed == false)
.Count(),
Revenue = Database.Query<Opportunity>()
.Where(o => o.Stage == "Closed Won")
.ToList()
.Sum(o => o.Amount)
};
return Json(stats, JsonRequestBehavior.AllowGet);
}
// API action - refreshes cached data
[HttpPost]
[HandleExceptionsForJson]
public ActionResponse RefreshData()
{
// Refresh logic here...
return Json(new { success = true, timestamp = DateTime.Now });
}
}
JavaScript in Active Page calls API:
// On the Dashboard Active Page
fetch('/acls/Dashboard/GetStats')
.then(response => response.json())
.then(data => {
document.getElementById('totalAccounts').textContent = data.TotalAccounts;
document.getElementById('totalContacts').textContent = data.TotalContacts;
// ... update other stats
});
Common Pitfalls and Solutions
❌ Pitfall 1: Trying to use View() in Standalone Controllers
Problem:
// URL: /acls/MyApi/GetData
public class MyApiController : AspxController
{
public ActionResponse GetData()
{
return View(); // ERROR: No Active Page exists!
}
}
Solution: Use an appropriate response type for standalone controllers:
public ActionResponse GetData()
{
var data = Database.Query<Contact>().ToList();
return Json(data, JsonRequestBehavior.AllowGet); // ✅ Correct
}
❌ Pitfall 2: Forgetting Action Name in /acls/ URLs
Problem:
/acls/ContactApi
This will fail because standalone controllers require an explicit action name.
Solution:
/acls/ContactApi/GetContact?id=123
❌ Pitfall 3: Not Handling JSON Exceptions
Problem:
public ActionResponse GetContact(string id)
{
// May throw exception
var contact = Database.Retrieve<Contact>(id);
return Json(contact, JsonRequestBehavior.AllowGet);
}
If an error occurs, the response is not JSON-friendly.
Solution: Use [HandleExceptionsForJson] attribute:
[HandleExceptionsForJson]
public ActionResponse GetContact(string id)
{
var contact = Database.Retrieve<Contact>(id);
return Json(contact, JsonRequestBehavior.AllowGet);
}
❌ Pitfall 4: Incorrect Controller Naming for Page Controllers
Problem:
// Active Page Name: ContactManager
public class ContactsController : AspxController // ❌ Wrong name!
{
public override ActionResponse Index()
{
return View();
}
}
Solution: Match the Active Page name exactly:
// Active Page Name: ContactManager
public class ContactManagerController : AspxController // ✅ Correct!
{
public override ActionResponse Index()
{
return View();
}
}
❌ Pitfall 5: Assuming Standalone Controllers Don't Require Authentication
Problem: Assuming /acls/ endpoints are publicly accessible without authentication.
Solution: All controllers require authentication by default. For public webhooks or external integrations, you may need to:
- Implement custom authentication logic
- Use API tokens
- Configure security appropriately
Debugging Tips
1. Check Controller Name Matches Active Page
For page controllers, verify:
- Active Page name:
ContactManager - Controller class name:
ContactManagerController
2. Verify URL Pattern
- Page Controller:
/aspx/PageName/Action - Standalone Controller:
/acls/ControllerName/Action
3. Add Logging to Actions
public ActionResponse MyAction(string id)
{
// Log to debug
SystemInfo.Debug($"MyAction called with id: {id}");
// Your logic here
return View();
}
4. Check ModelState for Validation Errors
[HttpPost]
public ActionResponse Create(Contact model)
{
if (!ModelState.IsValid)
{
// Log validation errors
foreach (var error in ModelState.Values.SelectMany(v => v.Errors))
SystemInfo.Debug($"Validation error: {error.ErrorMessage}");
}
return View(model);
}
5. Use Try-Catch for Debugging
public ActionResponse MyAction()
{
try
{
// Your logic
return View();
}
catch (Exception ex)
{
// Log exception details
SystemInfo.Debug($"Error: {ex.Message}");
SystemInfo.Debug($"Stack trace: {ex.StackTrace}");
return RedirectToError(ex.Message);
}
}
Architecture Decision Guide
Use this flowchart to decide which controller type to use:
┌─────────────────────────────────────┐
│ Do you need to render HTML/UI? │
└──────────────┬──────────────────────┘
│
┌──────┴──────┐
│ Yes │
└──────┬──────┘
│
┌──────────────▼───────────────────────┐
│ Use Page Controller (/aspx/) │
│ - Create Active Page │
│ - Controller renders View() │
│ - Use Magentrix UI components │
└──────────────┬───────────────────────┘
┌──────┴──────┐
│ No │
└──────┬──────┘
│
┌──────────────▼───────────────────────┐
│ What type of response needed? │
└──────────────┬───────────────────────┘
│
┌──────┴──────┐
│ │
┌───────▼────────┐ ┌─▼────────────────┐
│ JSON Data? │ │ File/PDF/Other? │
└───────┬────────┘ └─┬────────────────┘
│ │
│ │
┌───────▼─────────────▼──────────────────┐
│ Use Standalone Controller (/acls/) │
│ - No Active Page needed │
│ - Return Json(), File(), Pdf(), etc. │
│ - For APIs, webhooks, exports │
└────────────────────────────────────────┘
1. Database Query Optimization
❌ Inefficient:
public ActionResponse Index()
{
var accounts = Database.Query<Account>().ToList(); // Loads ALL accounts
return View(accounts);
}
✅ Optimized:
public ActionResponse Index()
{
// Take will limit results and read the fist 100 matching records.
var accounts = Database.Query<Account>()
.Where(a => a.IsActive == true)
.OrderBy(a => a.Name)
.Take(100)
.ToList();
return View(accounts);
}
2. Avoid Multiple Database Calls in Loops
❌ Inefficient:
foreach (var opportunityId in opportunityIds)
{
// N+1 query problem
var opp = Database.Retrieve<Opportunity>(opportunityId);
// Process opportunity
}
✅ Optimized:
var opportunities = Database.Query<Opportunity>()
.Where(o => opportunityIds.Contains(o.Id))
.ToList();
foreach (var opp in opportunities)
{
// Process opportunity
}
3. Use Stateless Controllers for APIs
For standalone API controllers, consider using stateless mode:
[SessionState(SessionStateMode.ReadOnly)]
public class ContactApiController : AspxController
{
// API actions here
}
This improves performance by reducing session overhead.
Security Best Practices
1. Always Validate Input
public ActionResponse ProcessData(string id)
{
// Validate input
if (string.IsNullOrEmpty(id))
return RedirectToError("Invalid ID parameter.");
// Proceed with logic
var record = Database.Retrieve<Contact>(id);
return View(record);
}
2. Use Authorization Attributes
[AuthorizeAction(Entity = "Opportunity", Action = StandardAction.Edit)]
public ActionResponse EditOpportunity(string id)
{
// Only users with Edit permission can access
var opp = Database.Retrieve<Opportunity>(id);
return View(opp);
}
3. Protect Against CSRF for POST Actions
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResponse DeleteAccount(string id)
{
Database.Delete<Account>(id);
return RedirectToAction("Index");
}
Summary
Custom controllers in Magentrix provide powerful server-side logic capabilities with two distinct patterns:
Page Controllers (/aspx/):
- Require corresponding Active Pages
- Render HTML user interfaces
- Use Magentrix UI components
- Perfect for interactive web applications
Standalone Controllers (/acls/):
- No Active Page required
- Return data (JSON, files, PDFs, etc.)
- Perfect for APIs, webhooks, and background jobs
- More flexible naming conventions
Both controller types:
- Inherit from
AspxController - Have full database access
- Support security and authorization
- Require authentication
- Can use all response types
Choose the right controller type based on whether you need to render HTML UI or provide programmatic data access. When in doubt, check if the Magentrix REST API already provides the functionality you need before building custom controllers.
💡
Ready to dive deeper? Continue to
Working with Controllers to learn about action methods, parameters, user information, cookies, and more advanced controller features.