Creating an OpsO App
This guide walks through creating a new OpsO Suite application from scratch using the DOM-based architecture.
Step 1: Create the Project
In Visual Studio, create a new "Class Library" project:
-
Project Name:
FlightPlan.{YourAppName}(e.g., FlightPlan.SpkrComp) - Location: C:\{Your VS Folder}\FlightPlan.{YourAppName}
- Check "Place solution and project in the same directory
- Click Next and choose Framework: .NET 8.0 (Long Term Support)
Step 2: Update the .csproj file
Click on the .csproj file, and edit it to match:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<BaseOutputPath>..\FlightPlan</BaseOutputPath>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\FlightPlan.Core\FlightPlan.Core.csproj" />
<ProjectReference Include="..\FlightPlan.OpsO\FlightPlan.OpsO.csproj" />
<ProjectReference Include="..\PixieDust.SqlServer\PixieDust.SqlServer.csproj" />
</ItemGroup>
</Project>
Step 3: Create App Structure
Create the following files:
3a. App Class
Extends OpsOWebAppBase<TBasePage>. Initialize, Shutdown, GetRoutes, RouteToPage, CreateNotFoundResponse, RedirectToLogin, and RedirectToAccessDenied are all inherited. Only define the four identity properties and the auth check in HandleRequest:
using FlightPlan.Core;
using FlightPlan.OpsO;
namespace FlightPlan.YourApp
{
public class YourAppApp : OpsOWebAppBase<YourAppBasePage>
{
protected override string PagesNamespace => "FlightPlan.YourApp.Pages";
public override string RoutePath => "/YourApp";
public override string AppName => "Your Application Name";
public override string Version => "1.0";
public override HttpResponse HandleRequest(HttpRequest request)
{
var userSession = AuthService.GetCurrentUser(request, "YourApp");
if (!userSession.IsAuthenticated)
return RedirectToLogin(request, "YourApp");
if (!userSession.Roles.Contains("read"))
return RedirectToAccessDenied("YourApp");
return RouteToPage(request);
}
}
}
3b. Base Page Class
Extends OpsOBasePage. Defines the navigation menu, optional footer contact, and any app-wide CSS. Individual pages set PageTitle in their own BuildContent().
using FlightPlan.Core.Dom.Styles;
using FlightPlan.OpsO;
namespace FlightPlan.YourApp
{
public abstract class YourAppBasePage : OpsOBasePage
{
protected override void ConfigureMasterPage()
{
FooterText = "For assistance contact:";
FooterLinkText = "Your Name";
FooterLinkUrl = "mailto:your.email@gsk.com";
MenuItems.Add(new MenuItem("Home", "/YourApp"));
if (HasRole("admin"))
{
var adminMenu = new MenuItem("Admin", "#");
adminMenu.SubItems.Add(new MenuItem("Manage Users", "/YourApp/ManageUsers"));
MenuItems.Add(adminMenu);
}
}
protected override void ConfigureAppStyles(StyleLibrary styles)
{
}
}
}
3c. Landing Page
Every page extends the app's BasePage and implements BuildContent(). RequiredRole gates the page; use MudID, Roles, and HasRole() for user-aware content:
using FlightPlan.Core.Dom;
using FlightPlan.Core.Dom.Elements;
namespace FlightPlan.YourApp.Pages
{
public class LandingPage : YourAppBasePage
{
protected override string RequiredRole => "read";
protected override void BuildContent(ContainerElement content)
{
PageTitle = "Your Application";
content.Add(new H1("Welcome to Your Application"));
content.Add(new P($"Logged in as: {MudID}"));
}
}
}
3d. Adapter Class (optional)
Static class for database queries. Add only if the app uses a database:
using PixieDust.SqlServer;
using System.Text;
namespace FlightPlan.YourApp
{
public static class YourAppAdapter
{
public static DbResults GetSomeData(string filter)
{
StringBuilder s = new StringBuilder();
s.AppendLine("SELECT * FROM YourTable");
s.AppendLine("WHERE category = @filter");
DbQuery q = new DbQuery(s.ToString());
q.AddParameter("@filter", filter);
return q.ExecuteReader();
}
}
}
Step 4: Build the DLL
Build the project in Release (or Debug) configuration and note the output path of the compiled DLL.
Step 5: Register the App
In the FlightPlan Server, open Manage Apps and add a new entry. The server stores registrations in apps.json. Each entry looks like:
{
"AssemblyPath": "C:\\path\\to\\FlightPlan.YourApp.dll",
"ClassName": "FlightPlan.YourApp.YourAppApp",
"RoutePath": "/YourApp",
"Enabled": true,
"DisplayOrder": 10
}
RoutePath must match the RoutePath property in your App class. ClassName must be the fully qualified class name.
Step 6: Database Security Setup
Add user permissions in the webuser_security table using: https://opso.gsk.com/Team/Access
Step 7: Test
Start (or restart) the server and navigate to /YourApp. You should see the landing page with the OpsO header, nav, and footer.
Architecture Notes
App Class Hierarchy
OpsO suite apps sit in a two-level inheritance chain:
- WebAppBase<TBasePage> (FlightPlan.Core) — routing, initialize, RouteToPage, CreateNotFoundResponse
- OpsOWebAppBase<TBasePage> (FlightPlan.OpsO) — adds RedirectToLogin, RedirectToAccessDenied
- YourAppApp — only the four identity properties and the HandleRequest auth check
Apps that have no security (e.g. public reference tools) extend WebAppBase directly and skip the OpsO layer.
Page Rendering Flow
- OpsOWebAppBase.HandleRequest() performs the app-level auth check, then calls RouteToPage()
- RouteToPage() resolves the path to a page type via RouteMap and calls page.Render()
- OpsOBasePage.Render() handles page-level auth (RequiredRole, RequiresAuth) and calls BuildDocument()
- BuildDocument() calls ConfigureMasterPage() and ConfigureAppStyles(), then BuildContent()
- BuildContent() populates the main content area with DOM elements
- The Document renders to HTML and is returned as an HttpResponse
Key DOM Element Classes
- ContainerElement — base for elements that contain children (Div, Form, Table, etc.)
- H1, H2, H3, P, Text — text elements
- Table, SimpleTable, Tr, Td, Th, Thead, Tbody — table elements
- Form, Input, Button, DropDownList, DatePicker — form elements
- A, Img, Br, Code, Pre — other common elements