Table of Contents


Advanced Controller Patterns

Advanced Controller Patterns

This section covers advanced patterns, best practices, and real-world scenarios for building robust, maintainable, and high-performance controllers in Magentrix.

Table of Contents

  1. Design Patterns
  2. Error Handling Strategies
  3. Performance Optimization
  4. Testing Controllers
  5. Common Pitfalls & Solutions
  6. Real-World Scenarios
  7. Migration & Maintenance

Design Patterns

Repository Pattern

Separate data access logic from business logic for better testability and maintainability.

Without Repository Pattern:

public class ContactController : AspxController
{
    public override ActionResponse Index()
    {
        // Data access mixed with controller logic
        var contacts = Database.Query<Contact>()
            .Where(c => c.IsActive)
            .OrderBy(c => c.LastName)
            .ToList();
        
        return View(contacts);
    }
}

With Repository Pattern:

// Repository class
public class ContactRepository
{
    public List<Contact> GetActiveContacts()
    {
        return Database.Query<Contact>()
            .Where(c => c.IsActive)
            .OrderBy(c => c.LastName)
            .ToList();
    }
    
    public Contact GetById(string id)
    {
        return Database.Retrieve<Contact>(id);
    }
    
    public List<Contact> SearchByName(string searchTerm)
    {
        return Database.Query<Contact>()
            .Where(c => c.FirstName.Contains(searchTerm) || 
                        c.LastName.Contains(searchTerm))
            .OrderBy(c => c.LastName)
            .ToList();
    }
    
    public void Create(Contact contact)
    {
        Database.Insert(contact);
    }
    
    public void Update(Contact contact)
    {
        Database.Update(contact);
    }
    
    public void Delete(string id)
    {
        Database.Delete(id);
    }
}

// Controller using repository
public class ContactController : AspxController
{
    private ContactRepository _repository = new ContactRepository();
    
    public override ActionResponse Index()
    {
        var contacts = _repository.GetActiveContacts();
        return View(contacts);
    }
    
    public ActionResponse Search(string query)
    {
        var results = _repository.SearchByName(query);
        return View("Index", results);
    }
}

Benefits:

  • Cleaner controller code
  • Reusable data access logic
  • Easier to test
  • Centralized query logic

Service Layer Pattern

Encapsulate business logic in service classes.

// Service class
public class OpportunityService
{
    public bool CanApprove(Opportunity opp, UserInfo user)
    {
        // Business rule: Closed opportunities can't be approved
        if (opp.IsClosed)
            return false;
        
        // Business rule: Only managers can approve
        if (!user.Roles.Contains("Sales Manager"))
            return false;
        
        // Business rule: High-value opportunities need director approval
        if (opp.Amount > 100000 && !user.Roles.Contains("Sales Director"))
            return false;
        
        return true;
    }
    
    public void Approve(Opportunity opp, UserInfo user)
    {
        if (!CanApprove(opp, user))
            throw new UnauthorizedAccessException("User cannot approve this opportunity.");
        
        opp.Status = "Approved";
        opp.ApprovedBy = user.Name;
        opp.ApprovedDate = DateTime.Now;
        
        Database.Update(opp);
        
        // Send notification email
        SendApprovalNotification(opp);
    }
    
    private void SendApprovalNotification(Opportunity opp)
    {
        // Email logic
    }
}

// Controller using service
public class OpportunityController : AspxController
{
    private OpportunityService _service = new OpportunityService();
    
    [HttpPost]
    [AuthorizeAction(Entity = "Opportunity", Action = StandardAction.Edit)]
    public ActionResponse Approve(string id)
    {
        try
        {
            var opp = Database.Retrieve<Opportunity>(id);
            
            if (!_service.CanApprove(opp, UserInfo))
            {
                AspxPage.AddError("You do not have permission to approve this opportunity.");
                return RedirectToAction("Details", new { id = id });
            }
            
            _service.Approve(opp, UserInfo);
            
            AspxPage.AddMessage("Opportunity approved successfully.");
            return RedirectToAction("Details", new { id = id });
        }
        catch (Exception ex)
        {
            SystemInfo.Error(ex);
            return RedirectToError("An error occurred during approval.");
        }
    }
}

Factory Pattern

Create objects dynamically based on conditions.

// Factory for creating different report generators
public class ReportGeneratorFactory
{
    public static IReportGenerator Create(string reportType)
    {
        switch (reportType.ToLower())
        {
            case "csv":
                return new CsvReportGenerator();
            case "pdf":
                return new PdfReportGenerator();
            case "excel":
                return new ExcelReportGenerator();
            default:
                throw new ArgumentException($"Unknown report type: {reportType}");
        }
    }
}

public interface IReportGenerator
{
    byte[] Generate(List<Account> data);
    string ContentType { get; }
    string FileExtension { get; }
}

public class CsvReportGenerator : IReportGenerator
{
    public string ContentType => "text/csv";
    public string FileExtension => ".csv";
    
    public byte[] Generate(List<Account> data)
    {
        var csv = new StringBuilder();
        csv.AppendLine("Name,Type,Industry,Revenue");
        
        foreach (var account in data)
            csv.AppendLine($"{account.Name},{account.Type},{account.Industry},{account.AnnualRevenue}");
        
        return Encoding.UTF8.GetBytes(csv.ToString());
    }
}

public class PdfReportGenerator : IReportGenerator
{
    public string ContentType => "application/pdf";
    public string FileExtension => ".pdf";
    
    public byte[] Generate(List<Account> data)
    {
        // PDF generation logic
        return PdfHelper.ConvertHtmlToPdf(GenerateHtml(data));
    }
    
    private string GenerateHtml(List<Account> data)
    {
        // HTML template logic
        return "...";
    }
}

// Controller using factory
public class ReportsController : AspxController
{
    public ActionResponse Export(string format)
    {
        try
        {
            var accounts = Database.Query<Account>().Limit().ToList();
            var generator = ReportGeneratorFactory.Create(format);
            var fileBytes = generator.Generate(accounts);
            var filename = $"accounts_report_{DateTime.Now:yyyyMMdd}{generator.FileExtension}";
            
            return File(fileBytes, generator.ContentType, filename);
        }
        catch (ArgumentException ex)
        {
            AspxPage.AddError(ex.Message);
            return RedirectToAction("Index");
        }
    }
}

Error Handling Strategies

Centralized Error Handling

Create a base controller with common error handling.

public class SecureController : AspxController
{
    protected ActionResponse HandleError(Exception ex, string userMessage = null)
    {
        // Log the error
        SystemInfo.Debug($"Error: {ex.Message}");
        SystemInfo.Error(ex);
        
        // Return user-friendly error
        var message = userMessage ?? "An error occurred. Please try again or contact support.";
        return RedirectToError(message);
    }
}

// Usage
public class ContactController : SecureController
{
    public ActionResponse Details(string id)
    {
        try
        {
            var contact = Database.Retrieve<Contact>(id);
            
            if (contact == null)
                return PageNotFound();
            
            return View(contact);
        }
        catch (Exception ex)
        {
            return HandleError(ex, "Unable to load contact details.");
        }
    }
}

Graceful Degradation

Handle failures gracefully without breaking the entire page.

public override ActionResponse Index()
{
    var viewModel = new DashboardViewModel();
    
    // Try to load statistics
    try
    {
        viewModel.TotalAccounts = Database.Query<Account>().Count();
        viewModel.TotalContacts = Database.Query<Contact>().Count();
    }
    catch (Exception ex)
    {
        EventLogProvider.Curre($"Stats error: {ex.Message}");
        viewModel.StatsError = "Unable to load statistics at this time.";
    }
    
    // Try to load recent activities
    try
    {
        viewModel.RecentActivities = Database.Query<Activity>()
            .OrderByDescending(a => a.CreatedDate)
            .Take(10)
            .ToList();
    }
    catch (Exception ex)
    {
        SystemInfo.Error(ex); 
        viewModel.ActivitiesError = "Unable to load recent activities.";
    }
    
    // Always return view, even with partial data
    return View(viewModel);
}

Performance Optimization

Database Query Optimization

❌ Bad - N+1 Query Problem:

public override ActionResponse Index()
{
    var opportunities = Database.Query<Opportunity>().ToList();
    
    foreach (var opp in opportunities)
    {
        // This executes a separate query for EACH opportunity!
        opp.Account = Database.Retrieve<Account>(opp.AccountId);
    }
    
    return View(opportunities);
}

✅ Good - Single Query with Related Data:

public override ActionResponse Index()
{
    // Retrieve opportunities
    var opportunities = Database.Query<Opportunity>().ToList();
    
    // Get all unique account IDs
    var accountIds = opportunities.Select(o => o.AccountId).Distinct().ToList();
    
    // Retrieve all accounts in one query
    var accounts = Database.Query<Account>()
        .Where(a => accountIds.Contains(a.Id))
        .ToList();
    
    // Create lookup dictionary
    var accountLookup = accounts.ToDictionary(a => a.Id);
    
    // Assign accounts to opportunities
    foreach (var opp in opportunities)
    {
        if (accountLookup.ContainsKey(opp.AccountId))
            opp.Account = accountLookup[opp.AccountId];
    }
    
    return View(opportunities);
}

Pagination

Without Pagination (Loads ALL records):

public override ActionResponse Index()
{
    // ❌ Loads thousands of records!
    var contacts = Database.Query<Contact>().ToList();
    return View(contacts);
}

With Pagination:

public override ActionResponse Index(int page = 1, int pageSize = 25)
{
    int skip = (page - 1) * pageSize;
    
    // Load only one page of records
    var contacts = Database.Query<Contact>()
        .OrderBy(c => c.LastName)
        .Skip(skip)
        .Take(pageSize)
        .ToList();
    
    // Get total count for pagination controls
    int totalCount = Database.Query<Contact>().Count();
    int totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
    
    // Pass pagination info to view
    DataBag.CurrentPage = page;
    DataBag.PageSize = pageSize;
    DataBag.TotalPages = totalPages;
    DataBag.TotalRecords = totalCount;
    
    return View(contacts);
}

Lazy Loading

Load data only when needed.

public class LazyDashboardController : AspxController
{
    // Initial page load - minimal data
    public override ActionResponse Index()
    {
        return View();
    }
    
    // AJAX endpoint - load statistics on demand
    [HandleExceptionsForJson]
    public ActionResponse GetStatistics()
    {
        var stats = new
        {
            TotalAccounts = Database.Query<Account>().Count(),
            TotalContacts = Database.Query<Contact>().Count(),
            OpenOpportunities = Database.Query<Opportunity>()
                .Where(o => !o.IsClosed)
                .Count()
        };
        
        return Json(stats, JsonRequestBehavior.AllowGet);
    }
    
    // AJAX endpoint - load activities on demand
    [HandleExceptionsForJson]
    public ActionResponse GetRecentActivities(int count = 10)
    {
        var activities = Database.Query<Activity>()
            .OrderByDescending(a => a.CreatedDate)
            .Take(count)
            .ToList();
        
        return Json(activities, JsonRequestBehavior.AllowGet);
    }
}

JavaScript on Active Page:

// Load statistics when page loads
fetch('/acls/LazyDashboard/GetStatistics')
    .then(response => response.json())
    .then(stats => {
        document.getElementById('totalAccounts').textContent = stats.TotalAccounts;
        document.getElementById('totalContacts').textContent = stats.TotalContacts;
    });

// Load activities when user clicks tab
document.getElementById('activitiesTab').addEventListener('click', function() {
    fetch('/acls/LazyDashboard/GetRecentActivities?count=20')
        .then(response => response.json())
        .then(activities => {
            renderActivities(activities);
        });
});

Testing Controllers

Manual Testing Checklist

For each controller action, test:

✅ Authenticated user with correct permissions
✅ Authenticated user without permissions
✅ Guest user (not logged in)
✅ Different security roles
✅ Portal users from different accounts
✅ Valid input data
✅ Invalid input data
✅ Missing required parameters
✅ NULL or empty values
✅ SQL injection attempts
✅ Cross-site scripting (XSS) attempts
✅ CSRF attack simulation
✅ Large datasets (performance)
✅ Concurrent requests
✅ Edge cases and boundary conditions

Test Data Setup

Create helper methods for test data.

public class TestDataHelper
{
    public static Contact CreateTestContact(string firstName = "Test", string lastName = "User")
    {
        var contact = new Contact
        {
            FirstName = firstName,
            LastName = lastName,
            Email = $"{firstName.ToLower()}.{lastName.ToLower()}@test.com",
            Phone = "(555) 123-4567"
        };
        
        Database.Insert(contact);
        return contact;
    }
    
    public static Account CreateTestAccount(string name = "Test Account")
    {
        var account = new Account
        {
            Name = name,
            Type = "Customer",
            Industry = "Technology"
        };
        
        Database.Insert(account);
        return account;
    }
    
    public static void CleanupTestData(string contactId = null, string accountId = null)
    {
        if (contactId != null)
            Database.Delete<Contact>(contactId);
        
        if (accountId != null)
            Database.Delete<Account>(accountId);
    }
}

Debugging Techniques

1. Add Detailed Logging:

public ActionResponse ComplexOperation(string id)
{
    SystemInfo.Debug($"[ComplexOperation] Starting for ID: {id}");
    SystemInfo.Debug($"[ComplexOperation] User: {UserInfo.Name}, IsPortalUser: {UserInfo.IsPortalUser}");
    
    try
    {
        var record = Database.Retrieve<Contact>(id);
        SystemInfo.Debug($"[ComplexOperation] Retrieved record: {record.FirstName} {record.LastName}");
        
        // Process record
        ProcessRecord(record);
        SystemInfo.Debug($"[ComplexOperation] Processing complete");
        
        return View(record);
    }
    catch (Exception ex)
    {
        SystemInfo.Error(ex);
        throw;
    }
}

2. Validate Assumptions:

public ActionResponse ProcessData(string accountId)
{
    // Validate assumption: accountId is not null
    if (string.IsNullOrEmpty(accountId))
    {
        SystemInfo.Debug("ERROR: accountId is null or empty");
        return RedirectToError("Invalid account ID.");
    }
    
    var account = Database.Retrieve<Account>(accountId);
    
    // Validate assumption: account exists
    if (account == null)
    {
        SystemInfo.Debug($"ERROR: Account not found: {accountId}");
        return PageNotFound();
    }
    
    // Validate assumption: user has access
    if (UserInfo.IsPortalUser && account.Id != UserInfo.Account.Id)
    {
        SystemInfo.Debug($"ERROR: User {UserInfo.Name} attempted to access account {accountId}");
        return UnauthorizedAccessResponse();
    }
    
    return View(account);
}

Common Pitfalls & Solutions

Pitfall 1: Not Checking for Null

❌ Problem:

public ActionResponse Details(string id)
{
    var contact = Database.Retrieve<Contact>(id);
    return View(contact); // Crashes if contact is null
}

✅ Solution:

public ActionResponse Details(string id)
{
    var contact = Database.Retrieve<Contact>(id);
    
    if (contact == null)
        return PageNotFound();
    
    return View(contact);
}

Pitfall 2: Returning View After POST

❌ Problem:

[HttpPost]
public ActionResponse Create(Contact model)
{
    Database.Insert(model);
    AspxPage.AddMessage("Contact created!");

    // Browser refresh will resubmit form!
    return View(model);
}

✅ Solution (PRG Pattern):

[HttpPost]
public ActionResponse Create(Contact model)
{
    Database.Insert(model);
    AspxPage.AddMessage("Contact created!");

    // Redirect after POST
    return RedirectToAction("Details", new { id = model.Id });
}

Pitfall 3: Loading Too Much Data

❌ Problem:

public override ActionResponse Index()
{
    // Loads 10,000+ records!
    var contacts = Database.Query<Contact>().Limit(50).ToList(); 
    return View(contacts);
}

✅ Solution:

public override ActionResponse Index(int page = 1)
{
    int pageSize = 25;
    var contacts = Database.Query<Contact>()
        .OrderBy(c => c.LastName)
        .Skip((page - 1) * pageSize)
        .Take(pageSize)
        .ToList();
    
    return View(contacts);
}

Pitfall 4: Not Validating User Input

❌ Problem:

public ActionResponse Transfer(string id, string newOwnerId)
{
    var opp = Database.Retrieve<Opportunity>(id);
    opp.OwnerId = newOwnerId; // Trusting user input!
    Database.Update(opp);
    return RedirectToAction("Index");
}

✅ Solution:

public ActionResponse Transfer(string id, string newOwnerId)
{
    // Validate opportunity exists
    var opp = Database.Retrieve<Opportunity>(id);

    if (opp == null)
        return PageNotFound();
    
    // Validate new owner exists and is active
    var newOwner = Database.Retrieve<User>(newOwnerId);

    if (newOwner == null || !newOwner.IsActive)
    {
        AspxPage.AddError("Invalid owner specified.");
        return RedirectToAction("Details", new { id = id });
    }
    
    // Validate user has permission
    if (UserInfo.IsPortalUser)
    {
        AspxPage.AddError("Portal users cannot transfer opportunities.");
        return RedirectToAction("Details", new { id = id });
    }
    
    opp.OwnerId = newOwnerId;
    Database.Update(opp);
    
    return RedirectToAction("Index");
}

Pitfall 5: Exposing Sensitive Information in Errors

❌ Problem:

catch (Exception ex)
{
    return RedirectToError(ex.Message); // Exposes internal details!
}

✅ Solution:

catch (Exception ex)
{
    SystemInfo.Error(ex);
    return RedirectToError("An error occurred. Please try again.");
}

Real-World Scenarios

Scenario 1: Multi-Step Approval Workflow

public class ApprovalWorkflowController : AspxController
{
    private OpportunityService _service = new OpportunityService();
    
    // View pending approvals
    [Authorize(Roles = "Sales Manager,Sales Director,VP Sales")]
    public override ActionResponse Index()
    {
        var opportunities = Database.Query<Opportunity>()
            .Where(o => o.Status == "Pending Approval")
            .OrderBy(o => o.SubmittedDate)
            .ToList();
        
        // Filter by approval level
        if (UserInfo.Roles.Contains("Sales Manager") && 
            !UserInfo.Roles.Contains("Sales Director"))
        {
            // Managers only see opportunities under $50K
            opportunities = opportunities.Where(o => o.Amount < 50000).ToList();
        }
        
        return View(opportunities);
    }
    
    // Approve opportunity
    [HttpPost]
    [ValidateAntiForgeryToken]
    [Authorize(Roles = "Sales Manager,Sales Director,VP Sales")]
    public ActionResponse Approve(string id, string comments)
    {
        try
        {
            var opp = Database.Retrieve<Opportunity>(id);
            
            if (opp == null)
                return PageNotFound();
            
            // Validate approval authority
            if (!_service.CanApprove(opp, UserInfo))
            {
                AspxPage.AddError("You do not have authority to approve this opportunity.");
                return RedirectToAction("Details", new { id = id });
            }
            
            // Check if additional approval needed
            var nextApprover = _service.GetNextApprover(opp);
            
            if (nextApprover != null)
            {
                // Route to next approver
                opp.Status = $"Pending {nextApprover} Approval";
                opp.CurrentApprover = nextApprover;
                Database.Update(opp);
                
                // Notify next approver
                _service.SendApprovalNotification(opp, nextApprover);
                
                AspxPage.AddMessage($"Opportunity routed to {nextApprover} for approval.");
            }
            else
            {
                // Final approval
                opp.Status = "Approved";
                opp.ApprovedBy = UserInfo.Name;
                opp.ApprovedDate = DateTime.Now;
                opp.ApprovalComments = comments;
                Database.Update(opp);
                
                // Notify submitter
                _service.SendFinalApprovalNotification(opp);
                
                AspxPage.AddMessage("Opportunity approved successfully.");
            }
            
            return RedirectToAction("Index");
        }
        catch (Exception ex)
        {
            SystemInfo.Error(ex);
            return RedirectToError("An error occurred during approval.");
        }
    }
    
    // Reject opportunity
    [HttpPost]
    [ValidateAntiForgeryToken]
    [Authorize(Roles = "Sales Manager,Sales Director,VP Sales")]
    public ActionResponse Reject(string id, string reason)
    {
        var opp = Database.Retrieve<Opportunity>(id);
        
        if (opp == null)
            return PageNotFound();
        
        opp.Status = "Rejected";
        opp.RejectedBy = UserInfo.Name;
        opp.RejectedDate = DateTime.Now;
        opp.RejectionReason = reason;
        Database.Update(opp);
        
        // Notify submitter
        _service.SendRejectionNotification(opp);
        
        AspxPage.AddMessage("Opportunity rejected.");
        return RedirectToAction("Index");
    }
}

Scenario 2: Batch Operations with Progress Tracking

public class BatchOperationsController : AspxController
{
    // Start batch operation
    [HttpPost]
    [Authorize(Roles = "Administrator")]
    public ActionResponse StartBatchUpdate(string criteria)
    {
        try
        {
            // Get records to update
            var contacts = Database.Query<Contact>()
                .Where(c => c.Status == criteria)
                .ToList();
            
            // Store batch job info
            var batchJob = new BatchJob
            {
                JobType = "Contact Update",
                TotalRecords = contacts.Count,
                ProcessedRecords = 0,
                Status = "In Progress",
                StartedBy = UserInfo.Name,
                StartedDate = DateTime.Now
            };
            
            Database.Insert(batchJob);
            
            // Process in background (simplified)
            ProcessBatchInBackground(batchJob.Id, contacts);
            
            AspxPage.AddMessage($"Batch job started. Processing {contacts.Count} records.");
            return RedirectToAction("ViewBatchJob", new { id = batchJob.Id });
        }
        catch (Exception ex)
        {
            SystemInfo.Error(ex);
            return RedirectToError("Failed to start batch operation.");
        }
    }
    
    // View batch job progress
    [Authorize(Roles = "Administrator")]
    public ActionResponse ViewBatchJob(string id)
    {
        var batchJob = Database.Retrieve<BatchJob>(id);
        
        if (batchJob == null)
            return PageNotFound();
        
        return View(batchJob);
    }
    
    // Get batch job status (AJAX)
    [HandleExceptionsForJson]
    [Authorize(Roles = "Administrator")]
    public ActionResponse GetBatchStatus(string id)
    {
        var batchJob = Database.Retrieve<BatchJob>(id);
        
        if (batchJob == null)
            return Json(new { error = "Job not found" }, 404, JsonRequestBehavior.AllowGet);
        
        return Json(new
        {
            status = batchJob.Status,
            totalRecords = batchJob.TotalRecords,
            processedRecords = batchJob.ProcessedRecords,
            successCount = batchJob.SuccessCount,
            errorCount = batchJob.ErrorCount,
            percentComplete = (batchJob.ProcessedRecords * 100) / batchJob.TotalRecords
        }, JsonRequestBehavior.AllowGet);
    }
    
    private void ProcessBatchInBackground(string batchJobId, List<Contact> contacts)
    {
        // In a real implementation, this would be a separate background process // For this example, we'll show the logic
        Task.Run(() =>
        {
            var batchJob = Database.Retrieve<BatchJob>(batchJobId);
            int successCount = 0;
            int errorCount = 0;
        
            foreach (var contact in contacts)
            {
                try
                {
                    // Perform update operation
                    contact.LastModifiedDate = DateTime.Now;
                    contact.LastModifiedBy = "Batch Process";
                    Database.Update(contact);
                
                    successCount++;
                }
                catch (Exception ex)
                {
                    SystemInfo.Error(ex);
                    errorCount++;
                }
            
                // Update progress every 10 records
                if ((successCount + errorCount) % 10 == 0)
                {
                    batchJob.ProcessedRecords = successCount + errorCount;
                    batchJob.SuccessCount = successCount;
                    batchJob.ErrorCount = errorCount;
                    Database.Update(batchJob);
                }
            }
        
            // Final update
            batchJob.ProcessedRecords = contacts.Count;
            batchJob.SuccessCount = successCount;
            batchJob.ErrorCount = errorCount;
            batchJob.Status = "Completed";
            batchJob.CompletedDate = DateTime.Now;
            Database.Update(batchJob);
        });
    }
}

// Poll for batch job status
function monitorBatchJob(jobId) {
    const interval = setInterval(() => {
        fetch(`/acls/BatchOperations/GetBatchStatus?id=${jobId}`)
            .then(response => response.json())
            .then(data => {
                // Update progress bar
                document.getElementById('progressBar').style.width = data.percentComplete + '%';
                document.getElementById('progressText').textContent = 
                    `${data.processedRecords} / ${data.totalRecords} records processed`;
                
                // Update counts
                document.getElementById('successCount').textContent = data.successCount;
                document.getElementById('errorCount').textContent = data.errorCount;
                
                // Stop polling when complete
                if (data.status === 'Completed') {
                    clearInterval(interval);
                    document.getElementById('statusMessage').textContent = 'Batch job completed!';
                }
            });
    }, 2000); // Poll every 2 seconds
}

Scenario 3: Complex Search with Filters

public class AdvancedSearchController : AspxController
{
    public override ActionResponse Index()
    {
        // Display search form
        PopulateFilterOptions();
        return View();
    }
    
    [HttpPost]
    public ActionResponse Search(SearchCriteria criteria)
    {
        try
        {
            // Build dynamic query
            var query = Database.Query<Opportunity>();
            
            // Apply filters
            if (!string.IsNullOrEmpty(criteria.Name))
                query = query.Where(o => o.Name.Contains(criteria.Name));
            
            if (!string.IsNullOrEmpty(criteria.Stage))
                query = query.Where(o => o.Stage == criteria.Stage);
            
            if (!string.IsNullOrEmpty(criteria.Type))
                query = query.Where(o => o.Type == criteria.Type);
            
            if (criteria.MinAmount.HasValue)
                query = query.Where(o => o.Amount >= criteria.MinAmount.Value);
            
            if (criteria.MaxAmount.HasValue)
                query = query.Where(o => o.Amount <= criteria.MaxAmount.Value);
            
            if (criteria.StartDate.HasValue)
                query = query.Where(o => o.CloseDate >= criteria.StartDate.Value);
            
            if (criteria.EndDate.HasValue)
                query = query.Where(o => o.CloseDate <= criteria.EndDate.Value);
            
            if (!string.IsNullOrEmpty(criteria.OwnerId))
                query = query.Where(o => o.OwnerId == criteria.OwnerId);
            
            // Apply account filter for portal users
            if (UserInfo.IsPortalUser)
                query = query.Where(o => o.AccountId == UserInfo.Account.Id);
            
            // Execute query with pagination
            int pageSize = 50;
            var results = query
                .OrderByDescending(o => o.CreatedDate)
                .Skip((criteria.Page - 1) * pageSize)
                .Take(pageSize)
                .ToList();
            
            // Get total count for pagination
            int totalCount = query.Count();
            
            // Prepare view model
            var viewModel = new SearchResultsViewModel
            {
                Results = results,
                Criteria = criteria,
                TotalCount = totalCount,
                PageSize = pageSize,
                TotalPages = (int)Math.Ceiling((double)totalCount / pageSize)
            };
            
            PopulateFilterOptions();
            return View("Results", viewModel);
        }
        catch (Exception ex)
        {
            SystemInfo.Error(ex);
            AspxPage.AddError("An error occurred during search. Please try again.");
            PopulateFilterOptions();
            return View("Index");
        }
    }
    
    // Export search results
    public ActionResponse ExportResults(SearchCriteria criteria)
    {
        // Rebuild query (same logic as Search action)
        var query = BuildSearchQuery(criteria);
        
        var results = query
            .OrderByDescending(o => o.CreatedDate)
            .ToList();
        
        // Generate CSV
        var csv = new StringBuilder();
        csv.AppendLine("Name,Stage,Type,Amount,Close Date,Owner");
        
        foreach (var opp in results)
            csv.AppendLine($"{opp.Name},{opp.Stage},{opp.Type},{opp.Amount},{opp.CloseDate:yyyy-MM-dd},{opp.OwnerName}");
        
        var bytes = Encoding.UTF8.GetBytes(csv.ToString());
        return File(bytes, "text/csv", $"search_results_{DateTime.Now:yyyyMMdd_HHmmss}.csv");
    }
    
    private IQueryable<Opportunity> BuildSearchQuery(SearchCriteria criteria)
    {
        var query = Database.Query<Opportunity>();
        
        if (!string.IsNullOrEmpty(criteria.Name))
            query = query.Where(o => o.Name.Contains(criteria.Name));
        
        if (!string.IsNullOrEmpty(criteria.Stage))
            query = query.Where(o => o.Stage == criteria.Stage);
        
        if (!string.IsNullOrEmpty(criteria.Type))
            query = query.Where(o => o.Type == criteria.Type);
        
        if (criteria.MinAmount.HasValue)
            query = query.Where(o => o.Amount >= criteria.MinAmount.Value);
        
        if (criteria.MaxAmount.HasValue)
            query = query.Where(o => o.Amount <= criteria.MaxAmount.Value);
        
        if (criteria.StartDate.HasValue)
            query = query.Where(o => o.CloseDate >= criteria.StartDate.Value);
        
        if (criteria.EndDate.HasValue)
            query = query.Where(o => o.CloseDate <= criteria.EndDate.Value);
        
        if (!string.IsNullOrEmpty(criteria.OwnerId))
            query = query.Where(o => o.OwnerId == criteria.OwnerId);
        
        if (UserInfo.IsPortalUser)
            query = query.Where(o => o.AccountId == UserInfo.Account.Id);
        
        return query;
    }
    
    private void PopulateFilterOptions()
    {
        // Populate dropdown options
        DataBag.Stages = new List<string> { "Prospecting", "Qualification", "Proposal", "Negotiation", "Closed Won", "Closed Lost" };
        DataBag.Types = new List<string> { "New Business", "Existing Business", "Renewal" };
        
        // Load owners
        var owners = Database.Query<User>()
            .Where(u => u.IsActive)
            .OrderBy(u => u.Name)
            .Select(u => new { u.Id, u.Name })
            .ToList();
        
        DataBag.Owners = owners;
    }
}

public class SearchCriteria
{
    public string Name { get; set; }
    public string Stage { get; set; }
    public string Type { get; set; }
    public decimal? MinAmount { get; set; }
    public decimal? MaxAmount { get; set; }
    public DateTime? StartDate { get; set; }
    public DateTime? EndDate { get; set; }
    public string OwnerId { get; set; }
    public int Page { get; set; } = 1;
}

Migration & Maintenance

Versioning Controllers

Use versioning for backward compatibility when making breaking changes.

// Version 1
public class ContactApiController : AspxController
{
    [HandleExceptionsForJson]
    public ActionResponse GetContact(string id)
    {
        var contact = Database.Retrieve<Contact>(id);
        return Json(new { 
            id = contact.Id,
            name = contact.Name,
            email = contact.Email 
        }, JsonRequestBehavior.AllowGet);
    }
}

// Version 2 - with additional fields
public class ContactApiV2Controller : AspxController
{
    [HandleExceptionsForJson]
    public ActionResponse GetContact(string id)
    {
        var contact = Database.Retrieve<Contact>(id);
        return Json(new { 
            id = contact.Id,
            firstName = contact.FirstName,
            lastName = contact.LastName,
            fullName = $"{contact.FirstName} {contact.LastName}",
            email = contact.Email,
            phone = contact.Phone,
            account = new {
                id = contact.AccountId,
                name = contact.Account.Name
            }
        }, JsonRequestBehavior.AllowGet);
    }
}

URLs:

  • V1: /acls/ContactApi/GetContact?id=123
  • V2: /acls/ContactApiV2/GetContact?id=123

Deprecation Strategy

Gracefully deprecate old endpoints.

public class LegacyApiController : AspxController
{
    [HandleExceptionsForJson]
    [Obsolete("This endpoint is deprecated. Use ContactApiV2 instead.")]
    public ActionResponse GetContact(string id)
    {
        // Log usage of deprecated endpoint
        LogDeprecatedEndpointUsage("GetContact", UserInfo.UserId);
        
        // Still provide functionality but warn users
        var contact = Database.Retrieve<Contact>(id);
        
        return Json(new { 
            deprecated = true,
            message = "This endpoint is deprecated. Please migrate to /acls/ContactApiV2/GetContact",
            migrationGuide = "https://docs.yourportal.com/api/migration",
            data = contact
        }, JsonRequestBehavior.AllowGet);
    }
    
    private void LogDeprecatedEndpointUsage(string endpoint, string userId)
    {
        try
        {
            var log = new DeprecationLog
            {
                Endpoint = endpoint,
                UserId = userId,
                Timestamp = DateTime.Now
            };
            
            Database.Insert(log);
        }
        catch
        {
            // Fail silently
        }
    }
}

Documentation Best Practices

Add XML comments to your controllers.

/// <summary>
/// Manages contact-related operations including CRUD and search.
/// </summary>
public class ContactController : AspxController
{
    /// <summary>
    /// Displays a paginated list of contacts.
    /// </summary>
    /// <param name="page">Page number (1-based)</param>
    /// <param name="pageSize">Number of records per page (default: 25)</param>
    /// <returns>Contact list view</returns>
    [AuthorizeAction(Entity = "Contact", Action = StandardAction.Read)]
    public override 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();
        
        return View(contacts);
    }
    
    /// <summary>
    /// Creates a new contact record.
    /// </summary>
    /// <param name="model">Contact data from form submission</param>
    /// <returns>Redirect to contact details on success, or form with errors on validation failure</returns>
    /// <remarks>
    /// Portal users: New contacts are automatically associated with the user's account.
    /// Employee users: Can associate contact with any account.
    /// </remarks>
    [HttpPost]
    [ValidateAntiForgeryToken]
    [AuthorizeAction(Entity = "Contact", Action = StandardAction.Create)]
    public ActionResponse Create(Contact model)
    {
        if (ModelState.IsValid)
        {
            // Portal users: force account association
            if (UserInfo.IsPortalUser)
                model.AccountId = UserInfo.Account.Id;
            
            Database.Insert(model);
            AspxPage.AddMessage("Contact created successfully!");
            return RedirectToAction("Details", new { id = model.Id });
        }
        
        return View(model);
    }
}

Summary

This comprehensive guide covered advanced controller patterns including:

Design Patterns - Repository, Service Layer, and Factory patterns for cleaner code

Error Handling - Centralized error handling, graceful degradation, and retry logic

Performance Optimization - Query optimization, pagination, caching, and lazy loading

Testing - Manual testing checklists, test data helpers, and debugging techniques

Common Pitfalls - Solutions for null checking, PRG pattern, data loading, validation, and error messages

Real-World Scenarios - Multi-step workflows, batch operations, and complex search

Migration & Maintenance - Versioning, deprecation strategies, and documentation


Final Checklist

Before deploying a controller to production:

Security

  • [ ] All actions have appropriate authorization
  • [ ] User input is validated
  • [ ] Account isolation enforced for portal users
  • [ ] CSRF protection on POST actions
  • [ ] No sensitive data exposed in errors

Performance

  • [ ] Database queries are optimized
  • [ ] Pagination implemented for large datasets
  • [ ] N+1 query problems eliminated
  • [ ] Expensive operations cached when appropriate

Error Handling

  • [ ] Try-catch blocks for external calls
  • [ ] User-friendly error messages
  • [ ] Errors logged for troubleshooting
  • [ ] Null checks before using objects

Testing

  • [ ] Tested with different user types
  • [ ] Tested with different security roles
  • [ ] Edge cases covered
  • [ ] Invalid input handled gracefully

Code Quality

  • [ ] Controllers follow single responsibility principle
  • [ ] Complex logic extracted to service classes
  • [ ] Code is well-commented
  • [ ] No hardcoded values

User Experience

  • [ ] Success messages displayed
  • [ ] Error messages are helpful
  • [ ] PRG pattern used for POST actions
  • [ ] Loading indicators for slow operations

Congratulations! 🎉

You've completed the comprehensive Magentrix Controller Developer Guide! You now have the knowledge to:

  • Build secure, high-performance controllers
  • Implement complex business logic
  • Handle errors gracefully
  • Test and maintain your code
  • Follow best practices and patterns

Next Steps

  1. Practice - Build real controllers in your Magentrix environment
  2. Experiment - Try different patterns and see what works best
  3. Review - Refer back to this guide when needed
  4. Share - Help other developers learn these concepts

Additional Resources

  • Magentrix REST API Documentation - For building integrations
  • Active Pages Guide - For creating the UI layer
  • Database & Entities Documentation - For data modeling

💡 Remember: Great controllers are secure, performant, maintainable, and user-friendly. Keep learning, keep improving, and happy coding!