Quick Start¶
A step-by-step guide to building a .NET equipment application with Port.
1. Overview¶
The Port Framework aims to enable flexible service architecture by seamlessly integrating physical documentation with system design.
In traditional development environments, we often encounter communication bottlenecks caused by:
- Inconsistent variable naming between hardware specifications and software code.
- The manual overhead of re-allocating variables whenever documentation changes.
- Ambiguous service designs that lead to development delays.
To resolve these issues, Port Framework automates the bridge between your specifications (.docx, .xlsx, *.csv) and implementation (.cs), ensuring a "Single Source of Truth."
2. Document Conversion (DOCX to Code)¶
The framework translates your specification tables into structured data models.
2.1 Source Specification¶
Assume a table exists in C:\Users\admin\Documents\IO.docx as follows:
| IO.No | Description | Model |
|---|---|---|
| D0.01 | Bulb1.OnOff | IODevice |
| D0.02 | Bulb2.OnOff | IODevice |
| A0.01 | Bulb1.Temp | IODevice |
| A0.02 | Bulb2.Temp | IODevice |
Figure 1: Sample Specification Table
2.2 Define Document Model¶
Map the table columns to a C# class using attributes.
public class IOModel
{
[ColumnHeader("IO.No"), EntryProperty]
public string IONo { get; set; } = null!;
[ColumnHeader("Description"), EntryKey]
public string Description { get; set; } = null!;
[ColumnHeader("Model"), EntryProperty]
public string Model { get; set; } = null!;
}
3. Project Configuration¶
3.1 Initialize Project¶
3.2 Page Files (.page)¶
The .page file is a collection of Entry definitions. Packages are referenced using the pkg: prefix.
[io.page]
Bulb1OnOff Enum.OnOff property:{"IO.No":"D0.01","Model":"IODevice"}
Bulb2OnOff Enum.OnOff property:{"IO.No":"D0.02","Model":"IODevice"}
Bulb1Temp f8 property:{"IO.No":"A0.01","Model":"IODevice"}
Bulb2Temp f8 property:{"IO.No":"A0.02","Model":"IODevice"}
3.3 Custom User Entries¶
You can define additional entries that are not part of the hardware document (e.g., logic setpoints) by creating localized .page files.
[bulb1/.page]
3.4 Generated Code Files (.cs)¶
The .cs file contains constant definitions for the entries, which can be used in your application code.
[io.cs]
// Auto-generated by Port. Do not edit manually.
namespace sample
{
public static class Io
{
public const string Bulb1OnOff = "Bulb1OnOff";
public const string Bulb2OnOff = "Bulb2OnOff";
public const string Bulb1Temp = "Bulb1Temp";
public const string Bulb2Temp = "Bulb2Temp";
}
}
3.5 Inline Entry Definitions with [Page]¶
For entries that don't come from an external document (e.g., EFEM I/O signals), define them
directly in a C# class decorated with [Page].
Auto-generated class (from Port.Pull)¶
Port.Pull writes a partial class file (e.g. entry.cs) containing every pulled entry as a
const string. This file is regenerated on every pull — do not edit it manually.
As of the latest update, entry.cs also includes a Defined class that contains all enum
definitions stored in the database, generated automatically alongside the entry constants.
// Auto-generated by Port.Pull — do not edit manually.
namespace Portdic
{
public partial class EFEM
{
public const string LP1_Main_Air_i = "EFEM.LP1_Main_Air_i";
public const string LP2_Cont_o = "EFEM.LP2_Cont_o";
// ... all pulled entries ...
}
public partial class Defined
{
public enum OffOn : int
{
Off = 0,
On = 1,
}
public enum UnkOffOn : int
{
Unknown = 0,
Off = 1,
On = 2,
}
// ... all enums from the database ...
}
}
User-defined extension (CustomEFEM)¶
Add entries and enum types that are not yet in the pulled class by creating a separate
[Page]-decorated partial class. The partial modifier lets both classes coexist without
conflicts.
Rules:
- The field value is the enum key — EnumName on [PageEntry] must match it exactly.
- [PageEnum] fields are pushed before [PageEntry] fields so enum references always resolve.
- Enum definitions land in app/.enum so port pull keeps them in the right file.
using Portdic;
using Portdic.SECS;
namespace sample.Controller
{
[Page("EFEM")]
public partial class CustomEFEM
{
// ── Enum declarations ─────────────────────────────────────────
// Field value = enum key referenced by EnumName below
[PageEnum("Unknown", "Off", "On")]
public const string UnkOffOn = "UnkOffOn";
[PageEnum("Unknown", "TurnOff", "TrunOn")]
public const string UnkTurnOffOn = "UnkTurnOffOn";
// ── Entry declarations ────────────────────────────────────────
// EnumName must match the field *value* of the [PageEnum] field
[PageEntry(PortDataType.Char)]
public const string LP1_Cont1_o = "EFEM.LP1_Cont1_o";
[PageEntry(PortDataType.Enum, EnumName = UnkTurnOffOn)]
public const string LP1_OffOn_o = "EFEM.LP1_OffOn_o";
// ── Package & Property binding ────────────────────────────────
// Package: "Name.PropertyName" → pushed as pkg:Name.PropertyName
// Property: raw JSON string → pushed as property:{...}
[PageEntry(PortDataType.Enum, EnumName = "OffOn",
Package = "Bulb1.OffOn",
Property = "{\"MIN\":0,\"MAX\":1}")]
public const string LP1_BulbOnOff_o = "EFEM.LP1_BulbOnOff_o";
}
}
Push the class instance before starting the port server:
Using both classes in a Model¶
[ModelBinding] attributes can freely mix entries from the auto-generated class and CustomEFEM:
[Model]
public class LoadportModel
{
// Entry added via CustomEFEM
[ModelBinding("LP1", CustomEFEM.LP1_Cont1_o)]
// Entry from the auto-generated EFEM class
[ModelBinding("LP2", EFEM.LP2_Cont_o)]
public Entry LP_Cont_o { get; set; }
[ModelBinding("LP1", CustomEFEM.LP1_OffOn_o)]
public Entry LP_OffOn_o { get; set; }
}
4. Push & Pull¶
4.1 Pushing Entries¶
Three overloads are available depending on the source of the entry data:
| Overload | Use case |
|---|---|
Push(reponame, obj) |
Push from a [Page]-decorated class instance |
Push(reponame, page) |
Push from a Page returned by Document<T>.NewPage() |
Push(repo) |
Push an entire directory via the port REST API (RepositoryInfo) |
// From a [Page]-decorated class (enums + entries)
Port.Push("sample", new CustomEFEM());
// From a document-derived Page (entries only)
Port.Push("sample", ioDoc.NewPage("Device"));
4.2 Pulling to a Directory¶
Port.Pull calls port pull {reponame} via the CLI and writes reconstructed .page, .enum,
and related files into a port/ subfolder inside the specified root directory.
// Syntax
Port.Pull(string reponame, string root);
// Example
Port.Pull("sample", @"D:\sample\Repo\pull\");
// → writes files to D:\sample\Repo\pull\port\
port.exe is resolved from the parent of %PortPath% first; if not found, it falls back to the
system PATH.
4.3 Full Initialization Pattern (DEBUG)¶
The recommended pattern in a #if DEBUG block synchronizes the DB with your latest class
definitions before loading the repository at runtime:
public MainWindow()
{
InitializeComponent();
#if DEBUG
// 1. Ensure project root exists
Port.Repository.New(@"D:\sample\Repo\pull\", "sample");
// 2. Convert external document to entries
var ioDoc = Port.Document<IOModel>(@"C:\Users\admin\Documents\IO.docx");
ioDoc.Where(v => v.Key.Contains("OnOff")).ToList().ForEach(v => v.DataType = "Enum.OnOff");
ioDoc.Where(v => v.Key.Contains("Temp")).ToList().ForEach(v => v.DataType = "f8");
if (ioDoc.Count > 0)
{
ioDoc.New(@"C:\Users\admin\Documents\sample\.page\io.page");
ioDoc.New(@"C:\Users\admin\Documents\sample\.net\io.cs");
}
// 3. Push inline-defined entries (enums + EFEM signals)
Port.Push("sample", new CustomEFEM());
// 4. Push document-derived entries
Port.Push("sample", ioDoc.NewPage("Device"));
// 5. Reconstruct .page/.enum files from DB
Port.Pull("sample", @"D:\sample\Repo\pull\");
#endif
// 6. Load repository and register controllers
if (Port.Repository.Load("sample"))
{
Port.Add<LoadportController, LoadportModel>("LP1");
Port.Add<LoadportController, LoadportModel>("LP2");
Port.OnReady += Port_OnReady;
Port.Run();
}
}
5. MCF Pattern (Model, Controller, Flow)¶
The MCF pattern decouples data (Model) from logic (Flow) via a centralized Controller.
5.1 ModelBinding the Model¶
Models use [ModelBinding] attributes to link software properties to the Entry keys defined in your documents.
[Model]
public class BulbModel
{
// Bind multiple instances (Bulb1, Bulb2) to the same property structure
[ModelBinding("Bulb1", Io.Bulb1OnOff)]
[ModelBinding("Bulb2", Io.Bulb2OnOff)]
public Entry OnOff { get; set; }
[ModelBinding("Bulb1", Io.Bulb1Temp)]
[ModelBinding("Bulb2", Io.Bulb2Temp)]
public Entry Temp { get; set; }
[ModelBinding("Bulb1", Io.Bulb1TargetTemp)]
[ModelBinding("Bulb2", Io.Bulb2TargetTemp)]
public Entry TargetTemp { get; set; }
}
5.2 Defining Logic with Flows¶
A Controller contains Flows, which are sequences of FlowSteps.
[Controller]
public class BulbController
{
[Flow("BulbOn")]
public class BulbOn
{
[FlowHandler]
public IFlowHandler handler { get; set; } = null!;
[FlowStep(0)] // Validation Step
public void CheckInitialState(BulbModel model)
{
if (model.Temp.Value <= 100)
handler?.Next();
}
[FlowStep(1)] // Action Step
public void TurnOn(BulbModel model)
{
model.OnOff.Set("On");
handler?.Next();
}
[FlowStep(2)] // Monitoring Step
public void MonitorTemperature(BulbModel model)
{
if (model.Temp.Value >= model.TargetTemp.Value)
{
model.OnOff.Set("Off");
handler?.Next(); // Marks Flow as Completed
}
}
}
}
6. Runtime & Repository Management¶
6.0 Application Entry Point (Port.App<T>)¶
Port.App<T>() is the mandatory first call when using the [Port] attribute-based
initialization style. Decorate your application class with [PortAttribute] to declare the
repository name and pull path, then call Port.App<T>() before any other Port API.
[Port("sample")]
public class SampleApp { }
// In startup (e.g. constructor or Program.cs) — must be called first
Port.App<SampleApp>();
Port.App<T>() reads the [Port] attribute on T, creates the repository via
Port.Repository.New(pullPath, reponame), and starts the PortDic instance.
All subsequent Port.Push, Port.Pull, and Port.Repository.Load calls depend on this
initialization being completed.
6.1 Loading the Repository¶
Synchronize the repository and initialize the runtime components.
[Port("sample")]
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
try
{
Port.App<MainWindow>();
#if DEBUG
var ioDoc = Port.Document<IOModel>(@"C:\Users\admin\Documents\IO.docx");
ioDoc.Where(v => v.Key.Contains("OnOff")).ToList().ForEach(v => v.DataType = "Enum.OnOff");
ioDoc.Where(v => v.Key.Contains("Temp")).ToList().ForEach(v => v.DataType = "f8");
if (ioDoc.Count > 0)
{
ioDoc.New(@"C:\Users\admin\Documents\sample\.page\io.page");
ioDoc.New(@"C:\Users\admin\Documents\sample\.net\io.cs");
}
Port.Push("sample", new CustomEFEM());
Port.Push("sample", ioDoc.NewPage("Device"));
Port.Pull("sample", @"D:\PORT\SampleArduinoLib\sample\Repo\pull\");
#endif
Port.Add<SessionHelper>("Session");
Port.Add<GemHelper>("GEM");
Port.Add<LoadportController, LoadportModel>("LP1");
Port.Add<LoadportController, LoadportModel>("LP2");
Port.Add<WTRController, WTRCommModel>("WTR");
Port.Add<JobController, JobModel>("Job");
//
Port.OnReady += Port_OnReady;
Port.Run();
}
catch (Exception ex)
{
MessageBox.Show($"{ex.Message}\n\nInner: {ex.InnerException?.Message}\n\nStack: {ex.StackTrace}");
}
}
...
6.2 Fast In-Memory Access¶
Access and control entries in real-time with high performance.