Table of Contents


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

  1. What Are Custom Controllers?
  2. Two Types of Controllers
  3. URL Structure Comparison
  4. Controller Architecture
  5. AspxController Base Class
  6. Creating Page Controllers (with Active Page)
  7. Creating Standalone Controllers (without Active Page)
  8. Action Methods
  9. Passing Models to Views (Page Controllers Only)
  10. When to Use Page vs Standalone Controllers
  11. When to Use Controllers vs Magentrix REST API
  12. Common Controller Patterns
  13. Controller Security and Authentication
  14. Next Steps
  15. Quick Reference
  16. Key Takeaways
  17. Practical Examples Summary
  18. Common Pitfalls and Solutions
  19. Debugging Tips
  20. Architecture Decision Guide
  21. Performance Considerations
  22. Security Best Practices
  23. 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 TypeURL PatternActive Page Required Typical Use
Page Controller/aspx/{PageName}/{Action}✅ YesWeb pages, forms, dashboards
Standalone Controller     /acls/{ControllerName}/{Action}❌ NoAPIs, 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:

  1. Request Routing: Magentrix receives the HTTP request and parses the URL to determine the controller type (/aspx/ or /acls/), controller name, and action
  2. Controller Instantiation: The platform instantiates the controller class
  3. Authentication & Authorization: The system validates user authentication and checks security attributes
  4. Action Execution: The specified action method is invoked with parameters from the URL or request body
  5. Business Logic: The action executes custom logic (database queries, validation, calculations, etc.)
  6. Response Generation: The action returns an ActionResponse object (View, JSON, Redirect, File, etc.)
  7. 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 controllers
  • acls: Area identifier for standalone Active Class controllers
  • PageName/ControllerName: The name of your controller
  • ActionName: 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 data
  • Redirect()- Navigate to different URLs
  • File() - Stream file downloads
  • Pdf() - Generate dynamic PDFs
  • Content() - Return raw text/HTML/XML/CSS/JavaScript
  • StorageFile() - 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:

  1. Security Role Assignment - The Active Page or Active Class must be assigned to specific Security Roles in Setup
  2. 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:


Quick Reference

URL Patterns

Controller TypeURL PatternExample
Page Controller/aspx/{PageName}/{Action}/aspx/ContactManager/Edit?id=123
Standalone Controller/acls/{ControllerName}/{Action}/acls/ContactApi/GetContact?id=123

Essential Controller Methods

MethodPurpose
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

PropertyPurpose
DatabaseAccess database operations (Query, Retrieve, Insert, Update, Delete)
UserInfoCurrent user information (Name, Email, IsPortalUser, Account, Contact) 
SystemInfoSystem and portal information (IsGuestUser, PortalUrl, etc.)
AspxPagePage utilities (messages, cookies, parameters)
ModelStateForm validation state
DataBagTemporary data storage across requests

Common Attributes

AttributePurpose
[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         │
└────────────────────────────────────────┘

Performance Considerations

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.