Table of Contents


Working with Controllers

This section provides an in-depth guide to developing custom controllers in Magentrix. You'll learn how to create action methods, work with parameters, access user information, manage cookies, handle validation, and leverage the powerful utilities provided by the AspxController base class.

Table of Contents

  1. Action Methods
  2. HTTP Method Handling (GET vs POST)
  3. Working with Parameters
  4. Accessing User Information
  5. Working with Cookies
  6. Displaying Messages to Users
  7. Model Binding and Validation
  8. Using DataBag for Temporary Storage
  9. Database Operations
  10. Error Handling
  11. Summary
  12. Next Steps
  13. Quick Reference

Action Methods

Action methods are public methods in your controller that respond to HTTP requests. Each action executes business logic and returns an ActionResponse.

Basic Action Structure

public ActionResponse MyAction()
{
    // Your business logic here
    return View();
}

Action Method Rules

All public methods are actions - Any public method in your controller is automatically treated as an action

No method overloads - You cannot create multiple actions with the same name unless they handle different HTTP verbs

The Index() action is special:

  • For Page Controllers (/aspx/): It's the default action and cannot accept parameters
  • Must override the base class implementation
  • Accessed when no action is specified in the URL

Non-action methods - Make methods private if they shouldn't be accessible via URL:

public class MyController : AspxController
{
    // This IS an action - accessible via URL
    public ActionResponse PublicAction()
    {
        return View();
    }
    
    // This is NOT an action - helper method only
    private string HelperMethod()
    {
        return "Some value";
    }
}

The Index Action

For Page Controllers:

public class ContactManagerController : AspxController
{
    // URL: /aspx/ContactManager or /aspx/ContactManager/Index
    public override ActionResponse Index()
    {
        var contacts = Database.Query<Contact>().Limit(20).ToList();
        return View(contacts);
    }
}
Important: The Index() action cannot have parameters for Page Controllers.

For Standalone Controllers:

public class ContactApiController : AspxController
{
    // URL: /acls/ContactApi/Index (must be explicitly specified)
    public ActionResponse Index()
    {
        var contacts = Database.Query<Contact>().Limit(20).ToList();
        return Json(contacts, JsonRequestBehavior.AllowGet);
    }
}

Custom Action Methods

public class ContactManagerController : AspxController
{
    // URL: /aspx/ContactManager/Search?name=John
    public ActionResponse Search(string name)
    {
        var results = Database.Query<Contact>()
            .Where(c => c.FirstName.Contains(name) || c.LastName.Contains(name))
            .ToList();
        
        return View(results);
    }
    
    // URL: /aspx/ContactManager/Details?id=003R000000haXk3IAE
    public ActionResponse Details(string id)
    {
        var contact = Database.Retrieve<Contact>(id);
        return View(contact);
    }
    
    // URL: /aspx/ContactManager/Export
    public ActionResponse Export()
    {
        var contacts = Database.Query<Contact>().ToList();
        
        // Generate CSV
        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");
    }
}

HTTP Method Handling (GET vs POST)

By default, action methods respond to GET requests. Use attributes to restrict actions to specific HTTP verbs.

GET Actions (Default)

public class ContactManagerController : AspxController
{
    // This responds to GET requests by default
    public override ActionResponse Index()
    {
        return View(new Contact());
    }
    
    // This also responds to GET requests
    public ActionResponse Edit(string id)
    {
        var contact = Database.Retrieve<Contact>(id);
        return View(contact);
    }
}

POST Actions

Use the [HttpPost] attribute to restrict an action to POST requests only:

public class ContactManagerController : AspxController
{
    // GET: Display empty form
    public override ActionResponse Index()
    {
        return View(new Contact());
    }
    
    // POST: Process form submission
    [HttpPost]
    public ActionResponse Index(Contact model)
    {
        if (ModelState.IsValid)
        {
            Database.Insert(model);
            AspxPage.AddMessage("Contact created successfully!");
            return RedirectToAction("Index");
        }
        
        return View(model);
    }
}

GET and POST Pattern

The most common pattern is to have two actions with the same name - one for GET (display form) and one for POST (process form):

public class OpportunityController : AspxController
{
    // GET: /aspx/Opportunity/Create
    // Shows the create form
    public ActionResponse Create()
    {
        var model = new Opportunity
        {
            Stage = "Prospecting",
            CloseDate = DateTime.Now.AddDays(30)
        };
        
        return View(model);
    }
    
    // POST: /aspx/Opportunity/Create
    // Processes the submitted form
    [HttpPost]
    public ActionResponse Create(Opportunity model)
    {
        if (ModelState.IsValid)
        {
            Database.Insert(model);
            AspxPage.AddMessage("Opportunity created successfully!");
            return RedirectToAction("Index");
        }
        
        // If validation fails, redisplay the form with errors
        return View(model);
    }
}

AcceptVerbs Attribute

To allow an action to respond to multiple HTTP verbs:

[AcceptVerbs("GET", "POST")]
public ActionResponse FlexibleAction(string data)
{
    if (Request.HttpMethod == "POST")
    {
        // Handle POST logic
        return Json(new { success = true });
    }
    else
    {
        // Handle GET logic
        return View();
    }
}
Caution: If you mark an action to respond to multiple HTTP verbs, you cannot create overloads for the same action name.

Working with Parameters

Actions can accept parameters from the URL query string or the request body (for POST requests).

Query String Parameters

Parameters are automatically mapped from the URL to action method parameters:

// URL: /aspx/ContactManager/Search?firstName=John&lastName=Doe
public ActionResponse Search(string firstName, string lastName)
{
    var results = Database.Query<Contact>()
        .Where(c => c.FirstName == firstName && c.LastName == lastName)
        .ToList();
    
    return View(results);
}

Reading Parameters with AspxPage

You can also read parameters manually using AspxPage.GetParameter():

public ActionResponse MyAction()
{
    var id = AspxPage.GetParameter("id");
    var filter = AspxPage.GetParameter("filter");
    
    if (string.IsNullOrEmpty(id))
        return RedirectToError("ID parameter is required.");
    
    var contact = Database.Retrieve<Contact>(id);
    return View(contact);
}

Multiple Parameter Types

// URL: /aspx/Reports/Generate?accountId=001R0000&startDate=2025-01-01&includeDetails=true
public ActionResponse Generate(string accountId, DateTime startDate, bool includeDetails)
{
    var account = Database.Retrieve<Account>(accountId);
    
    var opportunities = Database.Query<Opportunity>()
        .Where(o => o.AccountId == accountId && o.CloseDate >= startDate)
        .ToList();
    
    if (includeDetails)
    {
        // Include detailed information
    }
    
    return Pdf(opportunities, $"Report_{account.Name}.pdf");
}

Magentrix automatically converts query string values to the appropriate types (string, int, bool, DateTime, etc.).

POST Body Parameters (Model Binding)

For POST requests, parameters are automatically bound from the form data:

[HttpPost]
public ActionResponse CreateContact(Contact model)
{
    // model is automatically populated from POST form data
    if (ModelState.IsValid)
    {
        Database.Insert(model);
        return Json(new { success = true, id = model.Id });
    }
    
    return Json(new { success = false, errors = ModelState });
}

Complex Parameter Examples

Example 1: Search with Multiple Filters

public ActionResponse AdvancedSearch(
    string name, 
    string type, 
    string industry, 
    int? minRevenue, 
    int? maxRevenue)
{
    var query = Database.Query<Account>();
    
    if (!string.IsNullOrEmpty(name))
        query = query.Where(a => a.Name.Contains(name));
    
    if (!string.IsNullOrEmpty(type))
        query = query.Where(a => a.Type == type);
    
    if (!string.IsNullOrEmpty(industry))
        query = query.Where(a => a.Industry == industry);
    
    if (minRevenue.HasValue)
        query = query.Where(a => a.AnnualRevenue >= minRevenue.Value);
    
    if (maxRevenue.HasValue)
        query = query.Where(a => a.AnnualRevenue <= maxRevenue.Value);
    
    var results = query.ToList();
    return View(results);
}

Example 2: Pagination Parameters

// URL: /aspx/ContactList/Index?page=2&pageSize=50
public ActionResponse Index(int page = 1, int pageSize = 25)
{
    int skip = (page - 1) * pageSize;
    
    var contacts = Database.Query<Contact>()
        .OrderBy(c => c.LastName)
        .Skip(skip)
        .Take(pageSize)
        .ToList();
    
    int totalCount = Database.Query<Contact>().Count();
    int totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
    
    var viewModel = new PaginatedViewModel
    {
        Items = contacts,
        CurrentPage = page,
        PageSize = pageSize,
        TotalPages = totalPages
    };
    
    return View(viewModel);
}

Accessing User Information

Controllers provide comprehensive access to information about the currently logged-in user through the UserInfo and SystemInfo properties.

UserInfo Properties

The UserInfo object provides information about the current user:

public ActionResponse MyProfile()
{
    // Access current user's email
    var email = UserInfo.Email;
    
    // Access current user's full name
    var name = UserInfo.Name;
    
    // Check if user is a portal user (Partner/Customer)
    var isPortalUser = UserInfo.IsPortalUser;
    
    // Check if user is an employee
    var isEmployee = !UserInfo.IsPortalUser;
    
    return View();
}

Accessing Portal User's Contact and Account

For portal users (Partner/Customer users), you can access their associated Contact and Account records:

public ActionResponse Dashboard()
{
    if (UserInfo.IsPortalUser)
    {
        // Access the user's Contact record
        var userContact = UserInfo.Contact;
        
        // Access the user's Account record
        var userAccount = UserInfo.Account;
        
        // Now you can access any field on these records
        var phone = userContact?.Phone;
        var accountName = userAccount?.Name;
        var mailingStreet = userContact?.MailingStreet;
        var accountType = userAccount?.Type;
        
        var viewModel = new DashboardViewModel
        {
            UserName = userContact?.Name,
            CompanyName = userAccount?.Name,
            Phone = phone
        };
        
        return View(viewModel);
    }
    else
    {
        // Employee user logic
        return View();
    }
}

SystemInfo Properties

The SystemInfo object provides portal-level information:

public ActionResponse SystemStatus()
{
    // Check if current user is a guest (not logged in)
    var isGuest = SystemInfo.IsGuestUser;
    
    // Get portal URL
    var portalUrl = SystemInfo.PortalUrl;
    
    // Other system information
    // [TODO: clarify - what other properties are available on SystemInfo?]
    
    return View();
}

Practical Examples

Example 1: Showing User-Specific Data

public override ActionResponse Index()
{
    if (UserInfo.IsPortalUser)
    {
        // Show only opportunities related to the user's account
        var opportunities = Database.Query<Opportunity>()
            .Where(o => o.AccountId == UserInfo.Account.Id)
            .OrderByDescending(o => o.CreatedDate)
            .ToList();
        
        return View(opportunities);
    }
    else
    {
        // Employee users see all opportunities
        var opportunities = Database.Query<Opportunity>()
            .OrderByDescending(o => o.CreatedDate)
            .ToList();
        
        return View(opportunities);
    }
}

Example 2: User-Specific Welcome Message

public override ActionResponse Index()
{
    string welcomeMessage;
    
    if (UserInfo.IsPortalUser)
        welcomeMessage = $"Welcome {UserInfo.Contact.FirstName}! Your account: {UserInfo.Account.Name}";
    else
        welcomeMessage = $"Welcome {UserInfo.Name}";
    
    DataBag.WelcomeMessage = welcomeMessage;
    return View();
}

Example 3: Restricting Access Based on User Type

public ActionResponse AdminDashboard()
{
    // Only allow employee users
    if (UserInfo.IsPortalUser)
        return UnauthorizedAccessResponse();
    
    // Employee-only logic
    return View();
}

Example 4: Logging User Activity

[HttpPost]
public ActionResponse UpdateOpportunity(Opportunity model)
{
    if (ModelState.IsValid)
    {
        // Track who made the update
        model.LastModifiedBy = UserInfo.Name;
        model.LastModifiedDate = DateTime.Now;
        
        Database.Update(model);
        
        AspxPage.AddMessage("Opportunity updated successfully.");
        return RedirectToAction("Details", new { id = model.Id });
    }
    
    return View(model);
}

Working with Cookies

Controllers provide utilities for reading, writing, and removing browser cookies through the AspxPage object.

Reading Cookies

public ActionResponse MyAction()
{
    // Read a cookie
    var cookie = AspxPage.GetCookie("myCookieName");
    
    if (cookie != null)
    {
        var value = cookie.Value;
        // Use the cookie value
    }
    else
    {
        // Cookie doesn't exist
    }
    
    return View();
}

Writing Cookies

public ActionResponse SetPreferences(string theme, string language)
{
    // Create a new cookie
    var themeCookie = new HttpCookie("userTheme");
    themeCookie.Value = theme;
    themeCookie.Expires = DateTime.Now.AddDays(30); // Cookie expires in 30 days
    
    // Save the cookie
    AspxPage.SetCookie(themeCookie);
    
    // Create another cookie
    var langCookie = new HttpCookie("userLanguage");
    langCookie.Value = language;
    langCookie.Expires = DateTime.Now.AddYears(1); // Cookie expires in 1 year
    
    AspxPage.SetCookie(langCookie);
    
    AspxPage.AddMessage("Preferences saved!");
    return RedirectToAction("Index");
}

Updating Cookies

public ActionResponse UpdateCookie()
{
    // Read existing cookie
    var cookie = AspxPage.GetCookie("visitCount");
    
    if (cookie != null)
    {
        // Update the value
        int count = int.Parse(cookie.Value);
        count++;
        cookie.Value = count.ToString();
    }
    else
    {
        // Create new cookie if it doesn't exist
        cookie = new HttpCookie("visitCount");
        cookie.Value = "1";
        cookie.Expires = DateTime.Now.AddYears(1);
    }
    
    // Save the updated cookie
    AspxPage.SetCookie(cookie);
    
    return View();
}

Removing Cookies

public ActionResponse Logout()
{
    // Remove a cookie
    AspxPage.RemoveCookie("userTheme");
    AspxPage.RemoveCookie("userLanguage");
    AspxPage.RemoveCookie("visitCount");
    
    return Redirect("~/login");
}

Practical Cookie Examples

Example 1: Remember User Preferences

public override ActionResponse Index()
{
    // Check if user has a theme preference
    var themeCookie = AspxPage.GetCookie("userTheme");
    
    if (themeCookie != null)
        DataBag.Theme = themeCookie.Value;
    else
        DataBag.Theme = "default";
    
    return View();
}

Example 2: Track Last Visit

public override ActionResponse Index()
{
    HttpCookie lastVisitCookie = AspxPage.GetCookie("lastVisit");
    
    if (lastVisitCookie != null)
    {
        var lastVisit = DateTime.Parse(lastVisitCookie.Value);
        var timeSinceVisit = DateTime.Now - lastVisit;
        
        DataBag.WelcomeMessage = $"Welcome back! Your last visit was {timeSinceVisit.Days} days ago.";
    }
    else
        DataBag.WelcomeMessage = "Welcome! This is your first visit.";
    
    // Update last visit cookie
    var newVisitCookie = new HttpCookie("lastVisit");
    newVisitCookie.Value = DateTime.Now.ToString();
    newVisitCookie.Expires = DateTime.Now.AddYears(1);
    AspxPage.SetCookie(newVisitCookie);
    
    return View();
}

Example 3: Shopping Cart Cookie

public ActionResponse AddToCart(string productId)
{
    var cartCookie = AspxPage.GetCookie("shoppingCart");
    
    List<string> cartItems;
    if (cartCookie != null)
    {
        // Deserialize existing cart
        cartItems = JsonConvert.DeserializeObject<List<string>>(cartCookie.Value);
    }
    else
        cartItems = new List<string>();
    
    // Add product to cart
    cartItems.Add(productId);
    
    // Save updated cart
    var updatedCookie = new HttpCookie("shoppingCart");
    updatedCookie.Value = JsonConvert.SerializeObject(cartItems);
    updatedCookie.Expires = DateTime.Now.AddDays(7);
    AspxPage.SetCookie(updatedCookie);
    
    return Json(new { success = true, itemCount = cartItems.Count });
}

Displaying Messages to Users

Controllers can display formatted messages on Active Pages using AspxPage methods. These messages appear in a consistent, user-friendly format at the top of the page.

Message Types

Magentrix supports three types of messages:

  1. Error Messages (red) - For validation errors and failures
  2. Warning Messages (orange) - For cautionary information
  3. Informational Messages (blue) - For success confirmations and general info

Displaying Error Messages

public ActionResponse Save(Contact model)
{
    if (string.IsNullOrEmpty(model.Email))
    {
        // Show an error message at the top of the page
        AspxPage.AddError("Email address is required.");
        return View(model);
    }
    
    Database.Insert(model);
    return RedirectToAction("Index");
}

Displaying Field-Specific Errors

You can associate error messages with specific fields:

[HttpPost]
public ActionResponse Create(Contact model)
{
    // Validate email format
    if (!model.Email.Contains("@"))
    {
        // Show error next to the Email field
        AspxPage.AddError("Invalid email format.", "Email");
        return View(model);
    }
    
    // Validate phone number
    if (string.IsNullOrEmpty(model.Phone))
    {
        AspxPage.AddError("Phone number is required.", "Phone");
        return View(model);
    }
    
    Database.Insert(model);
    return RedirectToAction("Index");
}

The second parameter ("Email", "Phone") specifies the field key to associate the error with.

Displaying Success Messages

[HttpPost]
public ActionResponse Create(Contact model)
{
    if (ModelState.IsValid)
    {
        Database.Insert(model);
        
        // Show a success message (blue background)
        AspxPage.AddMessage("Contact created successfully!");
        
        return RedirectToAction("Index");
    }
    
    return View(model);
}

Displaying Warning Messages

public ActionResponse Delete(string id)
{
    var contact = Database.Retrieve<Contact>(id);
    
    // Check if contact has related records
    var opportunities = Database.Query<Opportunity>()
        .Where(o => o.ContactId == id)
        .Count();
    
    if (opportunities > 0)
    {
        // Show a warning message (orange background)
        AspxPage.AddWarning($"This contact has {opportunities} related opportunities. Deleting may affect data integrity.");
    }
    
    return View(contact);
}

Multiple Messages

You can display multiple messages of different types:

public ActionResponse ProcessBatch()
{
    int successCount = 0;
    int errorCount = 0;
    
    var contacts = Database.Query<Contact>().ToList();
    var contactsToUpdate = new List<Contact>();
    var failedContacts = new List<string>();
    
    foreach (var contact in contacts)
    {
        try
        {
            // Process contact (business logic only, no database calls)
            contact.LastProcessedDate = DateTime.Now;
            contact.Status = "Processed";
            
            contactsToUpdate.Add(contact);
            successCount++;
        }
        catch (Exception ex)
        {
            SystemInfo.Debug($"Error processing contact {contact.Id}: {ex.Message}");
            failedContacts.Add(contact.Id);
            errorCount++;
        }
    }
    
    // Single batch update operation
    if (contactsToUpdate.Count > 0)
    {
        Database.Update(contactsToUpdate);
    }
    
    if (successCount > 0)
    {
        AspxPage.AddMessage($"{successCount} contacts processed successfully.");
    }
    
    if (errorCount > 0)
    {
        AspxPage.AddError($"{errorCount} contacts failed to process.");
    }
    
    return RedirectToAction("Index");
}

Adding Messages in Active Pages

For messages to display, your Active Page must include the <aspx:ViewMessages> tag:

<aspx:AspxPage runat="server" title="Contact Manager">
    <body>
        <!-- This tag displays messages from the controller -->
        <aspx:ViewMessages runat="server"/>
        
        <aspx:ViewPanel runat='server' title='Contact Form'>
            <!-- Page content here -->
        </aspx:ViewPanel>
    </body>
</aspx:AspxPage>

Practical Message Examples

Example 1: Form Validation with Multiple Errors

[HttpPost]
public ActionResponse CreateOpportunity(Opportunity model)
{
    bool hasErrors = false;
    
    if (string.IsNullOrEmpty(model.Name))
    {
        AspxPage.AddError("Opportunity Name is required.", "Name");
        hasErrors = true;
    }
    
    if (model.Amount <= 0)
    {
        AspxPage.AddError("Amount must be greater than zero.", "Amount");
        hasErrors = true;
    }
    
    if (model.CloseDate < DateTime.Now)
    {
        AspxPage.AddError("Close Date cannot be in the past.", "CloseDate");
        hasErrors = true;
    }
    
    if (hasErrors)
        return View(model);
    
    Database.Insert(model);
    AspxPage.AddMessage("Opportunity created successfully!");
    return RedirectToAction("Index");
}

Example 2: Import Results with Mixed Outcomes

public ActionResponse ImportContacts(List<Contact> contacts)
{
    int imported = 0;
    int skipped = 0;
    int errors = 0;
    
    foreach (var contact in contacts)
    {
        try
        {
            // Check for duplicates
            var existing = Database.Query<Contact>()
                .Where(c => c.Email == contact.Email)
                .FirstOrDefault();
            
            if (existing != null)
            {
                skipped++;
                continue;
            }
            
            Database.Insert(contact);
            imported++;
        }
        catch (Exception)
        {
            errors++;
        }
    }
    
    if (imported > 0)
    {
        AspxPage.AddMessage($"{imported} contacts imported successfully.");
    }
    
    if (skipped > 0)
    {
        AspxPage.AddWarning($"{skipped} contacts were skipped (duplicates).");
    }
    
    if (errors > 0)
    {
        AspxPage.AddError($"{errors} contacts failed to import due to errors.");
    }
    
    return RedirectToAction("Index");
}

Model Binding and Validation

Magentrix automatically binds form data to C# objects and validates them using ModelState.

Automatic Model Binding

When a form is submitted (POST request), Magentrix automatically populates your model object with the form data:

[HttpPost]
public ActionResponse Create(Contact model)
{
    // 'model' is automatically populated with form data
    // model.FirstName, model.LastName, model.Email, etc.
    
    if (ModelState.IsValid)
    {
        Database.Insert(model);
        return RedirectToAction("Index");
    }
    
    return View(model);
}

Checking ModelState

ModelState.IsValid returns true if all validation rules pass:

[HttpPost]
public ActionResponse Save(Opportunity model)
{
    if (ModelState.IsValid)
    {
        // All validation passed
        Database.Update(model);
        AspxPage.AddMessage("Opportunity saved successfully.");
        return RedirectToAction("Details", new { id = model.Id });
    }
    else
    {
        // Validation failed - redisplay form with errors
        AspxPage.AddError("Please correct the errors below.");
        return View(model);
    }
}

Accessing Validation Errors

You can inspect individual validation errors:

[HttpPost]
public ActionResponse Create(Contact model)
{
    if (!ModelState.IsValid)
    {
        // Log all validation errors
        foreach (var key in ModelState.Keys)
        {
            var errors = ModelState[key].Errors;
            foreach (var error in errors)
            {
                SystemInfo.Debug($"Field: {key}, Error: {error.ErrorMessage}");
            }
        }
        
        AspxPage.AddError("Please correct the validation errors.");
        return View(model);
    }
    
    Database.Insert(model);
    return RedirectToAction("Index");
}

Accessing Old and New Values

When using the [SerializeViewData] attribute, you can access both the original value and the modified value:

[SerializeViewData]
public ActionResponse Edit(string id)
{
    var opportunity = Database.Retrieve<Opportunity>(id);
    DataBag.OriginalStage = opportunity.Stage;
    return View(opportunity);
}

[HttpPost]
public ActionResponse Edit(Opportunity model)
{
    if (ModelState.IsValid)
    {
        // Access original value
        var originalStage = DataBag.OriginalStage;
        
        // Access new value
        var newStage = model.Stage;
        
        // Check if stage changed
        if (originalStage != newStage)
        {
            AspxPage.AddMessage($"Stage changed from {originalStage} to {newStage}.");
        }
        
        Database.Update(model);
        return RedirectToAction("Index");
    }
    
    return View(model);
}

You can also use ModelState to access old and new values:

[HttpPost]
public ActionResponse Edit(Opportunity model)
{
    if (ModelState.IsValid)
    {
        // Access new value (user input)
        var newValue = ModelState["Amount"].Value.AttemptedValue;
        
        // Access old value (original from GET)
        var oldValue = ModelState["Amount"].Value.RawValue;
        
        Database.Update(model);
        return RedirectToAction("Index");
    }
    
    return View(model);
}

Manual Validation

You can add custom validation errors to ModelState:

[HttpPost]
public ActionResponse Create(Opportunity model)
{
    // Custom business rule validation
    if (model.Stage == "Closed Won" && model.Amount <= 0)
    {
        ModelState.AddModelError("Amount", "Amount must be greater than zero for Closed Won opportunities.");
    }
    
    if (model.CloseDate < DateTime.Now && model.Stage != "Closed Lost" && model.Stage != "Closed Won")
    {
        ModelState.AddModelError("CloseDate", "Close Date cannot be in the past for open opportunities.");
    }
    
    if (ModelState.IsValid)
    {
        Database.Insert(model);
        return RedirectToAction("Index");
    }
    
    return View(model);
}

Practical Validation Examples

Example 1: Complex Business Rule Validation

[HttpPost]
public ActionResponse CreateOpportunity(Opportunity model)
{
    // Validate required fields based on stage
    if (model.Stage == "Closed Won")
    {
        if (model.Amount <= 0)
        {
            ModelState.AddModelError("Amount", "Amount is required for Closed Won opportunities.");
        }
        if (model.CloseDate == null)
        {
            ModelState.AddModelError("CloseDate", "Close Date is required for Closed Won opportunities.");
        }
        if (string.IsNullOrEmpty(model.AccountId))
        {
            ModelState.AddModelError("AccountId", "Account is required for Closed Won opportunities.");
        }
    }

    if (model.Stage == "Closed Lost")
    {
        if (string.IsNullOrEmpty(model.LostReason))
        {
            ModelState.AddModelError("LostReason", "Lost Reason is required for Closed Lost opportunities.");
        }
    }

    // Validate date range
    if (model.CloseDate.HasValue && model.CloseDate.Value < DateTime.Now.AddDays(-1))
    {
        ModelState.AddModelError("CloseDate", "Close Date cannot be more than 1 day in the past.");
    }

    // Validate discount limit
    if (model.Discount > 25)
    {
        ModelState.AddModelError("Discount", "Discounts over 25% require manager approval.");
    }

    if (ModelState.IsValid)
    {
        Database.Insert(model);
        AspxPage.AddMessage("Opportunity created successfully!");
        return RedirectToAction("Index");
    }

    AspxPage.AddError("Please correct the validation errors below.");
    return View(model);
}

Example 2: Cross-Field Validation


[HttpPost]
public ActionResponse CreateContract(Contract model)
{
    // Validate that end date is after start date
    if (model.EndDate <= model.StartDate)
    {
        ModelState.AddModelError("EndDate", "End Date must be after Start Date.");
    }
    
    // Validate contract value based on account type
    var account = Database.Retrieve(model.AccountId);
    if (account.Type == "Small Business" && model.ContractValue > 50000)
    {
        ModelState.AddModelError("ContractValue", "Contract value exceeds the limit for Small Business accounts.");
    }
    
    // Validate renewal contract requires original contract
    if (model.Type == "Renewal" && string.IsNullOrEmpty(model.OriginalContractId))
    {
        ModelState.AddModelError("OriginalContractId", "Original Contract is required for Renewal contracts.");
    }
    
    if (ModelState.IsValid)
    {
        Database.Insert(model);
        AspxPage.AddMessage("Contract created successfully!");
        return RedirectToAction("Details", new { id = model.Id });
    }
    
    return View(model);
}

Example 3: Unique Value Validation

[HttpPost]
public ActionResponse CreateAccount(Account model)
{
    // Check for duplicate account name
    var existingAccount = Database.Query<Account>()
        .Where(a => a.Name == model.Name)
        .FirstOrDefault();
    
    if (existingAccount != null)
    {
        ModelState.AddModelError("Name", "An account with this name already exists.");
    }
    
    // Check for duplicate email
    if (!string.IsNullOrEmpty(model.Email))
    {
        var emailExists = Database.Query<Account>()
            .Where(a => a.Email == model.Email)
            .Any();
        
        if (emailExists)
        {
            ModelState.AddModelError("Email", "This email address is already registered to another account.");
        }
    }
    
    if (ModelState.IsValid)
    {
        Database.Insert(model);
        AspxPage.AddMessage("Account created successfully!");
        return RedirectToAction("Index");
    }
    
    return View(model);
}

Using DataBag for Temporary Storage

DataBag is a dynamic property bag that allows you to pass temporary data from controllers to views or persist data across GET and POST requests when using the [SerializeViewData] attribute.

Basic DataBag Usage

public override ActionResponse Index()
{
    // Store values in DataBag
    DataBag.PageTitle = "Welcome to Contact Manager";
    DataBag.ShowWelcomeMessage = true;
    DataBag.ItemCount = 42;
    
    return View();
}

Accessing in Active Page:

<aspx:AspxPage runat='server' title='{!DataBag.PageTitle}'>
    <body>
        <!-- Use DataBag values in the page -->
        <h1>{!DataBag.PageTitle}</h1>
        
        <aspx:Condition runat='server' test='{!DataBag.ShowWelcomeMessage}'>
            <p>Welcome! You have {!DataBag.ItemCount} items.</p>
        </aspx:Condition>
    </body>
</aspx:AspxPage>

Passing Complex Objects

public ActionResponse Dashboard()
{
    var stats = new DashboardStats
    {
        TotalAccounts = Database.Query<Account>().Count(),
        TotalContacts = Database.Query<Contact>().Count(),
        OpenOpportunities = Database.Query<Opportunity>()
            .Where(o => o.IsClosed == false)
            .Count()
    };
    
    DataBag.Statistics = stats;
    DataBag.LastUpdated = DateTime.Now;
    
    return View();
}

Persisting DataBag Across Requests

Use the [SerializeViewData] attribute to persist DataBag values across GET and POST requests:

[SerializeViewData]
public ActionResponse Edit(string id)
{
    var contact = Database.Retrieve<Contact>(id);
    
    // Store additional data in DataBag
    DataBag.PageTitle = $"Edit Contact: {contact.FirstName} {contact.LastName}";
    DataBag.OriginalEmail = contact.Email;
    DataBag.EditStartTime = DateTime.Now;
    
    return View(contact);
}

[HttpPost]
public ActionResponse Edit(Contact model)
{
    if (ModelState.IsValid)
    {
        // DataBag values are still available from the GET request
        var originalEmail = DataBag.OriginalEmail;
        var editStartTime = (DateTime)DataBag.EditStartTime;
        
        // Check if email changed
        if (originalEmail != model.Email)
        {
            AspxPage.AddMessage($"Email address changed from {originalEmail} to {model.Email}.");
        }
        
        // Calculate edit duration
        TimeSpan duration = DateTime.Now - editStartTime;
        SystemInfo.Debug($"Edit took {duration.TotalSeconds} seconds");
        
        Database.Update(model);
        return RedirectToAction("Index");
    }
    
    return View(model);
}

DataBag vs Model

Use Model when:

  • Passing entity data to the view
  • Working with database records
  • Form submission and model binding

Use DataBag when:

  • Passing metadata (page titles, flags, counts)
  • Storing temporary UI state
  • Passing lookup data (dropdown options, etc.)
  • Tracking workflow state across requests

Practical DataBag Examples

Example 1: Multi-Step Wizard

[SerializeViewData]
public ActionResponse Step1()
{
    DataBag.WizardStep = 1;
    DataBag.TotalSteps = 3;
    return View(new OpportunityWizardModel());
}

[HttpPost]
[SerializeViewData]
public ActionResponse Step1(OpportunityWizardModel model)
{
    if (ModelState.IsValid)
    {
        DataBag.Step1Data = model;
        DataBag.WizardStep = 2;
        return View("Step2", new ProductSelectionModel());
    }
    
    DataBag.WizardStep = 1;
    return View(model);
}

[HttpPost]
[SerializeViewData]
public ActionResponse Step2(ProductSelectionModel model)
{
    if (ModelState.IsValid)
    {
        DataBag.Step2Data = model;
        DataBag.WizardStep = 3;
        return View("Step3", new ReviewModel());
    }
    
    DataBag.WizardStep = 2;
    return View(model);
}

[HttpPost]
public ActionResponse Step3Confirm()
{
    // Retrieve data from all steps
    var step1Data = (OpportunityWizardModel)DataBag.Step1Data;
    var step2Data = (ProductSelectionModel)DataBag.Step2Data;
    
    // Create the opportunity
    var opportunity = new Opportunity
    {
        Name = step1Data.Name,
        AccountId = step1Data.AccountId,
        Amount = step2Data.TotalAmount,
        CloseDate = step1Data.CloseDate
    };
    
    Database.Insert(opportunity);
    
    AspxPage.AddMessage("Opportunity created successfully!");
    return RedirectToAction("Index");
}

Example 2: Passing Dropdown Options

public ActionResponse Create()
{
    // Populate dropdown options
    var accountTypes = new List<SelectListItem>
    {
        new SelectListItem { Text = "Partner", Value = "Partner" },
        new SelectListItem { Text = "Customer", Value = "Customer" },
        new SelectListItem { Text = "Prospect", Value = "Prospect" }
    };
    
    var industries = Database.Query<Account>()
        .Select(a => a.Industry)
        .Distinct()
        .OrderBy(i => i)
        .ToList();
    
    DataBag.AccountTypes = accountTypes;
    DataBag.Industries = industries;
    DataBag.PageTitle = "Create New Account";
    
    return View(new Account());
}

Example 3: Permission Flags

public ActionResponse Details(string id)
{
    var opportunity = Database.Retrieve<Opportunity>(id);
    
    // Set permission flags based on user role
    DataBag.CanEdit = CheckEditPermission(opportunity);
    DataBag.CanDelete = CheckDeletePermission(opportunity);
    DataBag.CanApprove = CheckApprovePermission(opportunity);
    
    // Set UI state
    DataBag.ShowWarning = opportunity.Amount > 100000;
    DataBag.WarningMessage = "High-value opportunity requires manager approval.";
    
    return View(opportunity);
}

private bool CheckEditPermission(Opportunity opp)
{
    // Check if user owns the opportunity or is a manager
    if (UserInfo.IsPortalUser)
    {
        return opp.AccountId == UserInfo.Account.Id;
    }
    return true; // Employees can always edit
}

private bool CheckDeletePermission(Opportunity opp)
{
    // Only allow deletion if opportunity is not closed
    return !opp.IsClosed;
}

private bool CheckApprovePermission(Opportunity opp)
{
    // Only employees can approve
    return !UserInfo.IsPortalUser;
}

Database Operations

Controllers have full access to database operations through the Database property.

Basic CRUD Operations

Create (Insert)

public ActionResponse CreateContact()
{
    var contact = new Contact
    {
        FirstName = "John",
        LastName = "Doe",
        Email = "john.doe@example.com",
        Phone = "(555) 123-4567"
    };
    
    Database.Insert(contact);
    
    // After insert, the contact.Id is populated
    AspxPage.AddMessage($"Contact created with ID: {contact.Id}");
    return RedirectToAction("Index");
}

Read (Retrieve)

public ActionResponse Details(string id)
{
    // Retrieve a single record by ID
    Contact contact = Database.Retrieve<Contact>(id);
    
    if (contact == null)
    {
        return PageNotFound();
    }
    
    return View(contact);
}

Update

[HttpPost]
public ActionResponse Edit(Contact model)
{
    if (ModelState.IsValid)
    {
        Database.Update(model);
        AspxPage.AddMessage("Contact updated successfully!");
        return RedirectToAction("Details", new { id = model.Id });
    }
    
    return View(model);
}

Delete

[HttpPost]
public ActionResponse Delete(string id)
{
    Database.Delete<Contact>(id);
    AspxPage.AddMessage("Contact deleted successfully.");
    return RedirectToAction("Index");
}

Querying Data

Simple Query

public ActionResponse Index()
{
    // Get all contacts
    var contacts = Database.Query<Contact>().ToList();
    return View(contacts);
}

Query with Filtering

public ActionResponse PartnerContacts()
{
    var contacts = Database.Query<Contact>()
        .Where(c => c.Account.Type == "Partner")
        .ToList();
    
    return View(contacts);
}

Query with Multiple Conditions

public ActionResponse SearchContacts(string name, string email)
{
    var query = Database.Query<Contact>();
    
    if (!string.IsNullOrEmpty(name))
    {
        query = query.Where(c => c.FirstName.Contains(name) || c.LastName.Contains(name));
    }
    
    if (!string.IsNullOrEmpty(email))
    {
        query = query.Where(c => c.Email.Contains(email));
    }
    
    var results = query.ToList();
    return View(results);
}

Query with Sorting

public ActionResponse Index()
{
    var contacts = Database.Query<Contact>()
        .OrderBy(c => c.LastName)
        .ThenBy(c => c.FirstName)
        .ToList();
    
    return View(contacts);
}

Query with Pagination

public ActionResponse Index(int page = 1, int pageSize = 25)
{
    int skip = (page - 1) * pageSize;
    
    var contacts = Database.Query<Contact>()
        .OrderBy(c => c.LastName)
        .Skip(skip)
        .Take(pageSize)
        .ToList();
    
    int totalCount = Database.Query<Contact>().Count();
    
    DataBag.CurrentPage = page;
    DataBag.PageSize = pageSize;
    DataBag.TotalPages = (int)Math.Ceiling((double)totalCount / pageSize);
    
    return View(contacts);
}

Aggregations

public ActionResponse Statistics()
{
    // Count records
    int totalAccounts = Database.Query<Account>().Count();
    
    // Sum values
    decimal totalRevenue = Database.Query<Opportunity>()
        .Where(o => o.Stage == "Closed Won")
        .Sum(o => o.Amount);
    
    // Average
    decimal avgDealSize = Database.Query<Opportunity>()
        .Where(o => o.Stage == "Closed Won")
        .Average(o => o.Amount);
    
    // Max/Min
    decimal largestDeal = Database.Query<Opportunity>()
        .Max(o => o.Amount);
    
    DataBag.TotalAccounts = totalAccounts;
    DataBag.TotalRevenue = totalRevenue;
    DataBag.AvgDealSize = avgDealSize;
    DataBag.LargestDeal = largestDeal;
    
    return View();
}

Related Records

Accessing Related Records

public ActionResponse AccountDetails(string id)
{
    // Retrieve account
    Account account = Database.Retrieve<Account>(id);
    
    // Get related contacts
    var contacts = Database.Query<Contact>()
        .Where(c => c.AccountId == id)
        .OrderBy(c => c.LastName)
        .ToList();
    
    // Get related opportunities
    var opportunities = Database.Query<Opportunity>()
        .Where(o => o.AccountId == id)
        .OrderByDescending(o => o.CreatedDate)
        .ToList();
    
    var viewModel = new AccountDetailsViewModel
    {
        Account = account,
        Contacts = contacts,
        Opportunities = opportunities
    };
    
    return View(viewModel);
}

Querying Through Relationships

public ActionResponse HighValueOpportunities()
{
    var opportunities = Database.Query<Opportunity>()
        .Where(o => o.Amount > 100000 && o.Account.Type == "Partner")
        .OrderByDescending(o => o.Amount)
        .ToList();
    
    return View(opportunities);
}

Bulk Operations

Bulk Insert

public ActionResponse ImportContacts(List<Contact> contacts)
{
    int imported = 0;
    int errors = 0;
    
    var contactsToInsert = new List<Contact>();
    var validationErrors = new List<string>();
    
    // Validate contacts (business logic only, no database calls)
    foreach (var contact in contacts)
    {
        try
        {
            // Perform validation
            if (string.IsNullOrWhiteSpace(contact.Email))
            {
                throw new Exception("Email is required");
            }
            
            contactsToInsert.Add(contact);
        }
        catch (Exception ex)
        {
            errors++;
            validationErrors.Add($"Contact {contact.FirstName} {contact.LastName}: {ex.Message}");
            SystemInfo.Debug($"Validation error: {ex.Message}");
        }
    }
    
    // Single batch insert operation
    if (contactsToInsert.Count > 0)
    {
        try
        {
            Database.Insert(contactsToInsert);
            imported = contactsToInsert.Count;
        }
        catch (Exception ex)
        {
            SystemInfo.Debug($"Batch insert error: {ex.Message}");
            AspxPage.AddError("An error occurred during import.");
            return RedirectToAction("Index");
        }
    }
    
    if (imported > 0)
    {
        AspxPage.AddMessage($"{imported} contacts imported successfully.");
    }
    
    if (errors > 0)
    {
        AspxPage.AddError($"{errors} contacts failed to import.");
    }
    
    return RedirectToAction("Index");
}

Bulk Update

public ActionResponse UpdateAccountType(string oldType, string newType)
{
    var accounts = Database.Query<Account>()
        .Where(a => a.Type == oldType)
        .ToList();
    
    foreach (var account in accounts)
    {
        account.Type = newType;
        Database.Update(account);
    }
    
    AspxPage.AddMessage($"{accounts.Count} accounts updated from {oldType} to {newType}.");
    return RedirectToAction("Index");
}

Advanced Query Patterns

Example 1: Complex Filter with Date Ranges

public ActionResponse OpportunityReport(DateTime startDate, DateTime endDate, string stage)
{
    var query = Database.Query<Opportunity>()
        .Where(o => o.CloseDate >= startDate && o.CloseDate <= endDate);
    
    if (!string.IsNullOrEmpty(stage))
    {
        query = query.Where(o => o.Stage == stage);
    }
    
    var opportunities = query
        .OrderBy(o => o.CloseDate)
        .ToList();
    
    decimal totalAmount = opportunities.Sum(o => o.Amount);
    
    DataBag.StartDate = startDate;
    DataBag.EndDate = endDate;
    DataBag.TotalAmount = totalAmount;
    DataBag.Count = opportunities.Count;
    
    return View(opportunities);
}

Example 2: Grouping and Aggregation

public ActionResponse SalesByRegion()
{
    var opportunities = Database.Query<Opportunity>()
        .Where(o => o.Stage == "Closed Won")
        .ToList();
    
    // Group by region and calculate totals
    var regionStats = opportunities
        .GroupBy(o => o.Account.Region)
        .Select(g => new RegionStats
        {
            Region = g.Key,
            TotalRevenue = g.Sum(o => o.Amount),
            DealCount = g.Count(),
            AverageDealSize = g.Average(o => o.Amount)
        })
        .OrderByDescending(r => r.TotalRevenue)
        .ToList();
    
    return View(regionStats);
}

Error Handling

Proper error handling ensures your controllers gracefully handle exceptions and provide meaningful feedback to users.

Try-Catch Pattern

public ActionResponse Details(string id)
{
    try
    {
        Contact contact = Database.Retrieve<Contact>(id);
        
        if (contact == null)
        {
            return PageNotFound();
        }
        
        return View(contact);
    }
    catch (Exception ex)
    {
        SystemInfo.Debug($"Error retrieving contact: {ex.Message}");
        return RedirectToError("An error occurred while retrieving the contact.");
    }
}

Handling Specific Exceptions

[HttpPost]
public ActionResponse Create(Contact model)
{
    try
    {
        if (ModelState.IsValid)
        {
            Database.Insert(model);
            AspxPage.AddMessage("Contact created successfully!");
            return RedirectToAction("Index");
        }
        
        return View(model);
    }
    catch (DuplicateRecordException)
    {
        AspxPage.AddError("A contact with this email already exists.");
        return View(model);
    }
    catch (ValidationException ex)
    {
        AspxPage.AddError($"Validation error: {ex.Message}");
        return View(model);
    }
    catch (Exception ex)
    {
        SystemInfo.Debug($"Unexpected error: {ex.Message}");
        return RedirectToError("An unexpected error occurred. Please try again.");
    }
}

Logging Errors

public ActionResponse ProcessBatch()
{
    try
    {
        var contacts = Database.Query<Contact>().ToList();
        
        foreach (var contact in contacts)
        {
            try
            {
                // Process each contact
                ProcessContact(contact);
            }
            catch (Exception ex)
            {
                // Log individual failures but continue processing
                SystemInfo.Debug($"Error processing contact {contact.Id}: {ex.Message}");
                SystemInfo.Error(ex);
            }
        }
        
        AspxPage.AddMessage("Batch processing complete.");
        return RedirectToAction("Index");
    }
    catch (Exception ex)
    {
        SystemInfo.Error(ex);
        return RedirectToError("Batch processing failed.");
    }
}

HandleExceptionsForJson Attribute

For API controllers that return JSON, use the [HandleExceptionsForJson] attribute to ensure errors are returned in JSON format:

[HandleExceptionsForJson]
public ActionResponse GetContact(string id)
{
    Contact contact = Database.Retrieve<Contact>(id);
    
    if (contact == null)
    {
        throw new NotFoundException("Contact not found");
    }
    
    return Json(contact, JsonRequestBehavior.AllowGet);
}

With this attribute, any exceptions are automatically caught and returned as JSON:

{
    "success": false,
    "error": "Contact not found"
}

Custom Error Responses

public ActionResponse Delete(string id)
{
    try
    {
        // Check if record has dependencies
        var dependentOpportunities = Database.Query<Opportunity>()
            .Where(o => o.ContactId == id)
            .Count();
        
        if (dependentOpportunities > 0)
        {
            return RedirectToError($"Cannot delete this contact because it has {dependentOpportunities} related opportunities. Please remove the opportunities first.");
        }
        
        Database.Delete<Contact>(id);
        AspxPage.AddMessage("Contact deleted successfully.");
        return RedirectToAction("Index");
    }
    catch (Exception ex)
    {
        SystemInfo.Error(ex);
        return RedirectToError("An error occurred while deleting the contact.");
    }
}

Practical Error Handling Examples

Example 1: File Upload with Error Handling

public ActionResponse UploadDocument(HttpPostedFileBase file)
{
    try
    {
        if (file == null || file.ContentLength == 0)
        {
            AspxPage.AddError("Please select a file to upload.");
            return View();
        }
        
        // Check file size (max 5MB)
        if (file.ContentLength > 5 * 1024 * 1024)
        {
            AspxPage.AddError("File size must be less than 5MB.");
            return View();
        }
        
        // Check file type
        string[] allowedExtensions = { ".pdf", ".doc", ".docx", ".xls", ".xlsx" };
        string extension = Path.GetExtension(file.FileName).ToLower();
        
        if (!allowedExtensions.Contains(extension))
        {
            AspxPage.AddError("Invalid file type. Allowed types: PDF, Word, Excel.");
            return View();
        }
        
        // Process file
        byte[] fileData = new byte[file.ContentLength];
        file.InputStream.Read(fileData, 0, file.ContentLength);
        
        // Save to storage
        string fileId = Storage.WriteFile(fileData, file.FileName, file.ContentType);
        
        AspxPage.AddMessage("File uploaded successfully!");
        return RedirectToAction("Index");
    }
    catch (Exception ex)
    {
        SystemInfo.Error(ex);
        return RedirectToError("An error occurred during file upload.");
    }
}

Example 2: External API Call with Error Handling

public ActionResponse SyncWithExternalSystem(string accountId)
{
    try
    {
        Account account = Database.Retrieve<Account>(accountId);
        
        if (account == null)
        {
            return PageNotFound();
        }
        
        // Convert account to JSON
        string jsonPayload = JsonHelper.ToJson(account);
        
        // Create HTTP request info
        var requestInfo = new HttpRequestInfo
        {
            Url = "https://api.external-system.com/accounts",
            ContentType = "application/json",
            PostData = jsonPayload,
            Timeout = 30000  // 30 seconds in milliseconds
        };
        
        // Call external API using HttpHelper
        var response = HttpHelper.Post(requestInfo);
        
        if (response.IsSuccessful)
        {
            AspxPage.AddMessage("Account synced successfully with external system.");
            return RedirectToAction("Details", new { id = accountId });
        }
        else
        {
            AspxPage.AddError($"External system returned error: {response.StatusCode}");
            SystemInfo.Debug($"API Error: {response.ErrorMessage}");
            return RedirectToAction("Details", new { id = accountId });
        }
    }
    catch (Exception ex)
    {
        SystemInfo.Error(ex);
        AspxPage.AddError("An error occurred during synchronization.");
        return RedirectToAction("Details", new { id = accountId });
    }
}

Summary

This section covered the essential aspects of working with custom controllers in Magentrix:

Action Methods - Public methods that respond to HTTP requests and return ActionResponse objects

HTTP Method Handling - Using GET for displaying pages and POST for processing forms

Working with Parameters - Reading URL parameters and binding POST data to models

Accessing User Information - Using UserInfo and SystemInfo to access current user data

Working with Cookies - Reading, writing, and removing browser cookies for state management

Displaying Messages - Showing errors, warnings, and success messages to users

Model Binding and Validation - Automatic form binding and using ModelState for validation

Using DataBag - Passing temporary data from controllers to views

Database Operations - Querying, inserting, updating, and deleting records

Error Handling - Gracefully handling exceptions and providing meaningful feedback


Next Steps

Continue your learning journey with these related topics:


Quick Reference

Essential Methods

Method/PropertyPurpose
Database.Query<T>()Query entity records
Database.Retrieve(id)Get single record by ID
Database.Insert(model)Create new record
Database.Update(model)Update existing record
Database.Delete(id)Delete record
UserInfo.EmailCurrent user's email
UserInfo.NameCurrent user's name
UserInfo.IsPortalUserCheck if portal user
UserInfo.ContactPortal user's Contact record
UserInfo.AccountPortal user's Account record
SystemInfo.IsGuestUserCheck if user is guest
AspxPage.GetParameter(key)Read URL parameter
AspxPage.GetCookie(name)Read browser cookie
AspxPage.SetCookie(cookie)Write browser cookie
AspxPage.RemoveCookie(name)Delete browser cookie
AspxPage.AddMessage(msg)Show success message
AspxPage.AddError(msg)Show error message
AspxPage.AddWarning(msg)Show warning message
ModelState.IsValidCheck if validation passed
DataBag.PropertyNameStore temporary data

Common Patterns

GET/POST Action Pair:

public ActionResponse Edit(string id)
{
    return View(Database.Retrieve(id));
}

[HttpPost]
public ActionResponse Edit(Contact model)
{
    if (ModelState.IsValid)
    {
        Database.Update(model);
        return RedirectToAction("Index");
    }
    return View(model);
}

Error Handling:

try
{
    // Your logic
    return View();
}
catch (Exception ex)
{
    EventLogProvider.Debug(ex.Message);
    return RedirectToError("An error occurred.");
}

User-Specific Data:

if (UserInfo.IsPortalUser)
{
    // Filter by user's account
    var data = Database.Query<Opportunity>()
        .Where(o => o.AccountId == UserInfo.Account.Id)
        .ToList();
}

💡 You now have a solid foundation for building custom controllers! Practice these patterns and explore the next sections to master controller responses, security, and advanced techniques.