Custom Controllers
The Maestro framework allows developers to create their own controller classes and execute them using the same dynamic loading mechanism exposed by QSAppContext.LoadDynamicController. These custom controllers can contain methods that return views, data, or any other type. They run in the same context as built‑in code blocks and have direct access to framework objects such as QSAppContext.
Custom controllers are stored as assemblies in a database cache and are loaded by name at runtime. This enables a plugin‑style architecture where new functionality can be introduced without redeploying the entire application.
Status: This feature is in active development. Routing attributes are planned but not yet fully operational. The documentation below reflects the current capabilities.
Overview
A custom controller is a class that contains methods which can be invoked through the Maestro runtime. It is particularly useful for encapsulating business logic that goes beyond simple data operations – for example, rendering a view, responding to an HTTP request, or performing a complex workflow that combines multiple data table actions.
Because the controller is loaded dynamically, its assembly is not referenced at compile time by the main application. Instead, it is stored in a database cache and resolved by name when needed.
Creating a Custom Controller
A custom controller is a plain class. It does not need to inherit from any specific base class or implement a particular interface. You can write it exactly as you would a normal C# class, and any public method becomes a candidate for invocation.
The controller can be stored as source code or compiled into an assembly that is placed in the database cache. Once available, it can be loaded via QSAppContext.LoadDynamicController and its methods invoked dynamically.
Access to Framework Objects
Inside a custom controller, the developer has direct access to all the framework objects that are available in regular code blocks. This includes:
QSAppContext– for data operations, function execution, batch saves, and controller loading.- Any other globals provided by the Maestro runtime.
This means you can call methods like QSAppContext.Tables["..."], QSAppContext.SaveChangesAsync(), or QSAppContext.ExecuteFunctionAsync(...) directly from within your controller methods.
Controller Methods & Return Types
Controller methods have no restrictions on their return type. They can return void, Task, IActionResult, string, dynamic, or any other .NET type. The framework serialises the result as needed when the method is invoked via an API or a view.
Example 1 – Async Data Operation
public async Task Test(string ArabicName, string EnglishName)This method updates a row in the NewForTest table using the Qss model and batch saving.
public async Task Test(string ArabicName, string EnglishName)
{
IQSDataTable _table = QSAppContext.Tables["NewForTest"];
Guid batchId = Guid.NewGuid();
IQSDataTableRow _row = await _table.GetRowBySerialAsync(10, batchId);
if (_row is not null)
{
_row["NameAr"] = ArabicName;
_row["NameEn"] = EnglishName;
await QSAppContext.SaveBatchOnlyAsync(batchId);
}
}Example 2 – Returning a View
public IActionResult TestView() This method returns a view, optionally passing a model object and ViewBag data. The view can later consume these values.
public IActionResult TestView()
{
var model = new { ArabicName = "arabic Data", EnglishName = "English Data" };
ViewBag.Age = 150;
return View("TestView", model);
}Example 3 – Simple String (API‑style)
[HttpPost]
public string Start() A minimal HTTP endpoint that returns a plain string. The [HttpPost] attribute is recognised, but full routing is still under development.
[HttpPost]
public string Start()
{
return "HHHHHELLLLLLO";
}Passing Data to Views (Model & ViewBag)
When a controller method returns a view, the developer can supply both a model object and ViewBag data. These mechanisms enable the controller to pass information to the view for rendering.
Model Object
The model object is provided as the second argument to the View method. It can be an anonymous type, a custom class, a dictionary, or any serialisable object. Inside the view, the model is accessible via the Model property. If the view is strongly typed (using a @model directive), developers get compile‑time checking; otherwise, the model is treated as dynamic.
ViewBag
ViewBag is a dynamic object that allows the controller to set arbitrary properties. These properties are then available in the view using the same property syntax. It is useful for passing small amounts of supplementary data that do not belong in the model.
View Access Example
The following snippet gives a preview of how a Razor‑style view might access the model and ViewBag values set in the previous TestView example.
@* Inside TestView.cshtml *@
@{
var arabic = Model.ArabicName;
var english = Model.EnglishName;
var age = ViewBag.Age;
}
<h2>@arabic - @english</h2>
<p>Age: @age</p>Routing (Placeholder)
Developers may use routing attributes such as [Route], [HttpGet], or [HttpPost] to define URL patterns for their controller methods. However, full routing support is not yet implemented. This section will be expanded once the feature becomes available. For now, methods are typically invoked through the dynamic controller instance or from a view context.
Registration & Discovery
Custom controller assemblies are stored in a database cache. When QSAppContext.LoadDynamicController is called, it looks up the controller by its simple name and loads the corresponding assembly. No explicit registration in code is required; the database cache is the source of truth.
To load a controller, simply pass its name:
dynamic myController = await QSAppContext.LoadDynamicController("MyCustomController");Usage in Views and APIs
View usage: A controller method that returns a view can be invoked to render a specific page. The view can access the model object and ViewBag values passed from the controller. Detailed instructions for the view side will be covered in the view documentation.
API usage: Methods that return primitive types, JSON, or other serialisable data can be called from client‑side code (e.g., JavaScript or external systems) once routing is fully implemented. Until then, they are mainly used from within the framework or via dynamic invocation.
Complete Example
A sample custom controller combining multiple method types:
// Example custom controller class
public class MyCustomController
{
// Async data operation using QSAppContext
public async Task UpdateRecord(string id, string name)
{
var table = QSAppContext.Tables["MyTable"];
var batchId = Guid.NewGuid();
var row = await table.GetRowByIdAsync(id, batchId);
if (row != null)
{
row["Name"] = name;
await QSAppContext.SaveBatchOnlyAsync(batchId);
}
}
// View method
public IActionResult Show()
{
ViewBag.Message = "Hello from custom controller";
return View("MyView", new { Title = "Custom Page" });
}
// API method
[HttpPost]
public string Ping()
{
return "Pong";
}
}