ZuloOne.Core Architecture
Updated
Overview#
ZuloOne.Core is the runtime engine and the heart of the ZuloOne platform. It provides metadata management, runtime code generation, CRUD operations, validation, events, and API endpoints.
📋 Note: This is a DESIGN document from 2025-11-02 describing a planned architecture. Most of the file names and controllers it lists were never built under those names — 31 of 38 named files do not exist. Read it for intent, not for orientation; for how the platform is actually laid out see wiki/developer/architecture.md.
The one section still worth reading as current is «initial subtype» (search for
Subtype = NULL): it explains why a null subtype is not neutral, and that is
still true.
Main Responsibilities#
- Loads metadata (
MetaDictionary,MetaDictionaryProperty,MetaDocumentHeaderField,MetaDocumentLine,MetaDocumentLineField,MetaDocumentStatus,MetaRegisterHeader,MetaRegisterField,MetaDataEntities,MetaScript). - Generates runtime classes via Roslyn.
- Manages CRUD, validation, and events.
- Provides API for Web and external systems (metadata and runtime access).
- Exposes metadata and runtime models to isolated processes (Scheduler, workers).
- Implements migrations and database schema synchronization (auto-applied on startup).
- Supports multiple database systems.
- Generates Metadata Packages for offline deployment.
Namespace and Folder Structure#
ZuloOne.Core
├── Metadata/
│ ├── MetaModel.cs // Models (Core, Tax, CRM, WHS, Custom, etc.)
│ ├── MetaEDT.cs // Extended Data Types
│ ├── MetaDictionary.cs // Dictionaries
│ ├── MetaDictionaryProperty.cs // Dictionary field definitions
│ ├── MetaDocumentType.cs // Document type definitions
│ ├── MetaDocumentHeaderField.cs // Document header field definitions
│ ├── MetaDocumentLine.cs // Document line type definitions (line types for documents)
│ ├── MetaDocumentLineField.cs // Document line field definitions
│ ├── MetaDocumentStatus.cs // Document status definitions (workflow states)
│ ├── MetaRegister.cs // Register type definitions (register type, engine, behavior)
│ ├── MetaRegisterHeader.cs // Register header field definitions
│ ├── MetaRegisterField.cs // Register field definitions (for line fields)
│ ├── MetaDataEntity.cs // Virtual entities for OData interface
│ ├── MetaLayer.cs // System layers (System, ISV, Customer)
│ ├── MetaScript.cs // Script storage (all C# code: events, extensions, DataEntities, etc.)
│ ├── MetaProject.cs // Development projects (isolated workspaces for developers)
│ ├── MetaProjectObject.cs // Many-to-many: projects ↔ metadata objects
│ ├── MetaPackage.cs // Package registry
│ ├── MetaEnum.cs // Enumerations
│ ├── MetaUser.cs // User accounts
│ ├── MetaRole.cs // Security roles
│ ├── MetaUserRole.cs // User-Role relationships
│ ├── MetaPermission.cs // Permission definitions
│ ├── MetaObjectPermission.cs // Object-level permissions
│ ├── MetaDataPermission.cs // Record-level data permissions
│ ├── MetaSession.cs // User sessions
│ ├── MetaAuditLog.cs // Audit trail
│ └── MetaMetadataVersion.cs // Metadata version history (for rollback)
├── Engine/
│ ├── RuntimeCompiler.cs
│ ├── ObjectFactory.cs
│ ├── EventDispatcher.cs
│ ├── DataContextBuilder.cs
│ ├── SchemaSynchronizer.cs
│ ├── RegisterEngine.cs // Register calculation engine interface
│ ├── RegisterEngineFactory.cs // Factory for creating register engines
│ ├── Engines/
│ │ ├── StandardRegisterEngine.cs // Standard accumulation engine
│ │ ├── FIFORegisterEngine.cs // FIFO calculation engine
│ │ ├── FinancialRegisterEngine.cs // Financial collapsing engine
│ │ ├── FlatHistoryRegisterEngine.cs // Flat history engine
│ │ └── CustomRegisterEngine.cs // Custom engine from scripts
│ └── RuntimeDomainLoader.cs // Hot-reload assemblies via AssemblyLoadContext
├── Services/
│ ├── DataService.cs
│ ├── MetadataService.cs
│ ├── EventService.cs
│ ├── ExtensionService.cs // Business logic extensions (stored in MetaScript)
│ ├── RegisterEngineService.cs // Register engine management and execution
│ ├── ScriptEngine.cs // CSharpScript compilation and execution
│ ├── MigrationService.cs
│ ├── ProjectService.cs // Development project management
│ ├── TestService.cs // Test execution and reporting
│ ├── PackageService.cs // Package management and installation
│ ├── AuthenticationService.cs // User authentication (login, password, sessions)
│ ├── AuthorizationService.cs // Permission checking and authorization
│ ├── UserService.cs // User management (CRUD operations)
│ ├── PermissionService.cs // Permission management
│ ├── AuditService.cs // Audit logging
│ └── MetadataPublisher.cs // Publishes MetadataChanged events
├── API/
│ ├── Controllers/
│ │ ├── DataController.cs
│ │ ├── MetadataController.cs
│ │ │ ├── GET /api/metadata/{objectType}/{objectMetaId}/versions
│ │ │ ├── GET /api/metadata/{objectType}/{objectMetaId}/versions/{version}
│ │ │ ├── POST /api/metadata/{objectType}/{objectMetaId}/rollback/{version}
│ │ │ └── GET /api/metadata/{objectType}/{objectMetaId}/compare?version1={v1}&version2={v2}
│ │ ├── EventController.cs
│ │ ├── ProjectController.cs // Project management API
│ │ ├── TestController.cs // Test execution API
│ │ ├── PackageController.cs // Package upload and installation
│ │ ├── AuthController.cs // Authentication API (login, logout, token refresh)
│ │ ├── UserController.cs // User management API
│ │ ├── RoleController.cs // Role management API
│ │ └── PermissionController.cs // Permission management API
│ ├── gRPC/
│ │ ├── MetadataService.cs // gRPC service for metadata access
│ │ └── RuntimeService.cs // gRPC service for runtime types
│ └── Startup.cs
└── ZuloOne.Core.csproj
ZuloOne.Runtime (Shared Library)
├── Metadata/
│ ├── MetadataLoader.cs // Load from API/DB/file
│ ├── MetadataRepository.cs // Metadata storage abstraction
│ └── MetadataVersion.cs // Version tracking
├── Runtime/
│ ├── TypeGenerator.cs // Generate types from metadata
│ ├── RuntimeLoader.cs // Load compiled assemblies
│ ├── AssemblyManager.cs // AssemblyLoadContext management
│ └── RuntimeDomainLoader.cs // Hot-reload support
├── Execution/
│ ├── ScriptEngine.cs // Execute event scripts
│ ├── ExtensionExecutor.cs // Execute extensions
│ └── DataProvider.cs // Unified data access
└── Services/
├── MetadataService.cs // Metadata operations
└── RuntimeService.cs // Runtime type operations
🧩 Metamodel#
The metamodel defines the structure of all business entities in the ERP system. All metadata objects are stored in the database.
ERP Metadata Objects#
The system supports standard ERP object types:
MetaModel— business logic models for functional separation- Core models (system-provided): Core, Tax, CRM, WHS
- Custom models (client-created): client-specific extensions
- Models provide namespace and security boundaries
- All metadata objects belong to a model (ModelId)
- Belongs to a Layer (LayerId) — System, ISV, or Customer layer
- Layer determines compilation order and access isolation
- Fields:
MetaId(Guid, primary key, auto-generated),Name,ModelId,LayerId, ...
MetaEDT(Extended Data Type) — extended data types with parameters (length, precision, scale, etc.)- Fields:
MetaId(Guid, primary key, auto-generated),Name,EDTType,Length,Precision, ...
- Fields:
MetaDictionary— dictionaries - simple lookup tables- Fields:
MetaId(Guid, primary key, auto-generated),Name,ModelId,LayerId,IsExtension,BaseDictionaryMetaId, ... - Extension support:
IsExtension=true,BaseDictionaryMetaIdreferences base dictionary MetaId
- Fields:
MetaDictionaryProperty— field definitions for dictionaries (separate table)- Fields:
MetaId(Guid, primary key, auto-generated),DictionaryMetaId(references MetaDictionary.MetaId),FieldName,EDTMetaId,IsRequired, etc.
- Fields:
MetaDocumentType— document type definitions (document types with header and lines)- Fields:
MetaId(Guid, primary key, auto-generated),Name,ModelId,LayerId,IsExtension,BaseDocumentTypeMetaId, ... - Extension support:
IsExtension=true,BaseDocumentTypeMetaIdreferences base document type MetaId
- Fields:
MetaDocumentHeaderField— header field definitions for documents (defines fields in document header, not the header itself)- Fields:
MetaId(Guid, primary key, auto-generated),DocumentTypeMetaId(references MetaDocumentType.MetaId),FieldName,EDTMetaId,IsRequired, etc. - Used for base document types and extensions (extension adds new header fields via this table)
- Fields:
MetaDocumentLine— document line type definitions (defines line types for documents)- Fields:
MetaId(Guid, primary key, auto-generated),DocumentTypeMetaId(references MetaDocumentType.MetaId),Name,Description,ModelId,LayerId,IsExtension,BaseDocumentLineMetaId, ... - When document type is created, system automatically generates default line type (e.g., "SalesOrderLine" for "SalesOrder" document)
- One document type can have multiple line types (e.g., InvoiceLine, InvoiceDiscountLine, InvoiceTaxLine)
- Extension support:
IsExtension=true,BaseDocumentLineMetaIdreferences base line type MetaId - System automatically generates C# class for line type in metadata with properties like DocumentType, LineNumber, etc.
- Fields:
MetaDocumentLineField— field definitions for document lines (defines fields in document line records, not the line records themselves)- Fields:
MetaId(Guid, primary key, auto-generated),DocumentLineMetaId(references MetaDocumentLine.MetaId),FieldName,EDTMetaId,IsRequired, etc. - Used for line field definitions (header fields use
MetaDocumentHeaderFieldtable) - Extension support: When document line type is extended, new line field definitions are added to this table with
DocumentLineMetaIdpointing to extension line type
- Fields:
MetaDocumentStatus— document status definitions for workflow (possible statuses for document types)- Fields:
MetaId(Guid, primary key, auto-generated),DocumentTypeMetaId(references MetaDocumentType.MetaId),StatusName,StatusValue,DisplayOrder,Description,IsInitial,IsFinal,WorkflowOrder, ... - Defines possible statuses for document types (e.g., "Draft", "Posted", "Cancelled" for Invoice; "Draft", "Started", "Completed", "Closed" for Inventory)
- Workflow order defines status transition sequence
- Example: For SalesOrder: "Draft" (IsInitial=true), "Confirmed", "Shipped", "Invoiced", "Completed" (IsFinal=true)
- Fields:
MetaRegister— register type definitions (registers with header and lines)
- Fields:
MetaId(Guid, primary key, auto-generated),Name,ModelId,LayerId,IsExtension,BaseRegisterMetaId,RegisterEngineType, ... - Extension support:
IsExtension=true,BaseRegisterMetaIdreferences base register MetaId
-
MetaRegisterHeader— register header field definitions (defines fields in register header, not the header itself)- Fields:
MetaId(Guid, primary key, auto-generated),RegisterMetaId(references MetaRegister.MetaId),FieldName,EDTMetaId, ...
- Fields:
-
MetaRegisterField— field definitions for registers (defines fields in register lines, not the lines themselves)- Fields:
MetaId(Guid, primary key, auto-generated),RegisterMetaId(references MetaRegister.MetaId),FieldName,FieldType("Header" or "Line"),EDTMetaId, ...
- Fields:
-
MetaEnum— enumeration definitions (simple key-value lists that compile to C# enums)- Fields:
MetaId(Guid, primary key, auto-generated),Name,ModelId,LayerId,IsExtension,BaseEnumMetaId,Description - Extension support:
IsExtension=true,BaseEnumMetaIdreferences base enum MetaId
- Fields:
-
MetaEnumValue— enum value definitions (text-value pairs)- Fields:
MetaId(Guid, primary key, auto-generated),EnumMetaId(references MetaEnum.MetaId),Name,Value(int),DisplayOrder,Description
- Fields:
-
MetaDataEntity— virtual entities for OData interface (data exposure to external systems)- Fields:
MetaId(Guid, primary key, auto-generated),Name,ModelId,LayerId,IsExtension,BaseDataEntityMetaId, ... - Extension support:
IsExtension=true,BaseDataEntityMetaIdreferences base DataEntity MetaId
- Fields:
-
MetaService— service class definitions (helper classes for business logic, no visual UI)- Fields:
MetaId(Guid, primary key, auto-generated),Name,Description,ModelId,LayerId,Namespace, ... - Service classes are C# classes in free form (with fields and methods)
- Code stored in
MetaScriptwithScriptType="ServiceClass" - No visual interface — services are pure code classes
- Can be used in business logic (events, extensions, other services)
- Can be debugged and compiled
- Example: LocationHelper service with methods for address calculations
- Fields:
-
MetaScript— script storage table (stores C# code: classes, scripts, processing logic)- Fields:
MetaId(Guid, primary key, auto-generated),Name,ScriptType,ObjectType,ObjectName,ObjectMetaId,EventName,BaseClassName,Code,ModelId, ... - Separate table for all scripts
- Referenced by metadata objects (DataEntity, Dictionary, Document, Register, Extension, Event, Service)
- Fields:
-
Event Handlers — each metadata object (Dictionary, Document, Register) automatically gets one event handler class stored in
MetaScript:MetaScript.ScriptType = "EventHandler"— identifies as event handler classMetaScript.ObjectType— object type (e.g., "Dictionary", "Document", "Register")MetaScript.ObjectName— object name (e.g., "Customer", "Invoice", "InventoryBalance")MetaScript.ObjectMetaId— MetaId (Guid) of the metadata object (references MetaDictionary.MetaId, MetaDocumentType.MetaId, or MetaRegister.MetaId)MetaScript.BaseClassName— base event handler class (e.g., "DictionaryEventHandler", "DocumentEventHandler", "RegisterEventHandler")MetaScript.Code— full C# class code with event handler methods (OnBeforeInsert, OnAfterInsert, etc.)- Automatic generation: When creating new Dictionary/Document/Register, system automatically creates corresponding event handler class in MetaScript
- One class per object: Each object has exactly one event handler class containing all its event methods
-
Business Logic Extensions — stored in
MetaScriptwithScriptType="ExtensionClass"MetaScript.Name— extension class name (e.g., "InvoicePostingExt")MetaScript.Code— full extension class C# codeMetaScript.BaseClassName— base class being extended (e.g., "InvoicePosting") - stored as metadata- Extends business logic classes (like InvoicePosting)
- C# code extensions with inheritance
- No separate MetaExtension table needed — extensions are just scripts with ScriptType="ExtensionClass"
-
MetaLayer— system layers for extension hierarchy- Fields:
Id,Name,LayerType(System, ISV, Customer),Order(compilation order) - Defines layer hierarchy: System (0) → ISV (1) → Customer (2)
- Controls compilation order for extension chains
- Fields:
-
MetaProject— development projects (isolated workspaces for developers)- Projects allow multiple developers to work independently
- Projects reference metadata objects via
MetaProjectObject(many-to-many) - One object can belong to multiple projects
- Projects can be exported/imported (dev → production)
- Projects are versioned and can be merged
-
MetaProjectObject— many-to-many relationship between projects and metadata objects- Fields:
Id,ProjectId,ObjectType(Dictionary, Document, Register, etc.),ObjectId - Allows same object to be in multiple projects
- Tracks which objects belong to which projects
- Fields:
-
Tests — unit and integration tests stored in
MetaScripttable- Test scripts stored in
MetaScriptwithScriptType="UnitTest"orScriptType="IntegrationTest" MetaScript.ObjectType— "Test"MetaScript.ObjectName— test name (e.g., "CustomerCreationTest")MetaScript.ObjectMetaId— references metadata object being tested (Dictionary, Document, Register, etc.)MetaScript.Code— full C# test code (uses test framework)- No separate MetaTest table — tests are scripts with special ScriptType
- Tests have dedicated editor in Web UI for test management and execution
- Test execution and reporting via TestService
- Test scripts stored in
-
MetaPackage— installed package registry (tracks package versions, dependencies) -
MetaScriptHistory— script version history and audit trail -
MetaJob— job definitions for Scheduler (references MetaScript) -
MetaUser— user accounts (system users)- Fields:
MetaId(Guid, PK),UserName,Email,PasswordHash,IsActive,FirstName,LastName,LastLogin,Created,Modified, ...
- Fields:
-
MetaRole— security roles (groups of permissions)- Fields:
MetaId(Guid, PK),Name,Description,IsSystem(system roles cannot be deleted),Created,Modified, ...
- Fields:
-
MetaUserRole— many-to-many relationship between users and roles- Fields:
MetaId(Guid, PK),UserMetaId(references MetaUser.MetaId),RoleMetaId(references MetaRole.MetaId),Assigned, ...
- Fields:
-
MetaPermission— permission definitions (types of permissions)- Fields:
MetaId(Guid, PK),Name,PermissionType(MetadataRead, MetadataWrite, DataRead, DataWrite, Execute, Compile, etc.),Description, ...
- Fields:
-
MetaObjectPermission— permissions on metadata objects (Dictionary, Document, Register, Service, etc.)- Fields:
MetaId(Guid, PK),RoleMetaId(references MetaRole.MetaId),ObjectType(Dictionary, Document, Register, Service, etc.),ObjectMetaId(references metadata object MetaId),PermissionType(Read, Write, Create, Delete, Execute, Compile),IsGranted(true=grant, false=deny), ...
- Fields:
-
MetaDataPermission— record-level data permissions (permissions on business data records)- Fields:
MetaId(Guid, PK),UserMetaId(references MetaUser.MetaId),RoleMetaId(references MetaRole.MetaId),TableName(e.g., "Customers", "Invoices"),RecordId(specific record Id, NULL for all records),PermissionType(Read, Write, Create, Delete, Post),Condition(optional C# expression for dynamic filtering), ...
- Fields:
-
MetaSession— user sessions (active user sessions)- Fields:
MetaId(Guid, PK),UserMetaId(references MetaUser.MetaId),SessionToken,IpAddress,UserAgent,Created,Expires,LastActivity,IsActive, ...
- Fields:
-
MetaAuditLog— audit trail (log of all user actions)- Fields:
MetaId(Guid, PK),UserMetaId(references MetaUser.MetaId),Action(Create, Update, Delete, Read, Execute, Login, Logout),ObjectType,ObjectMetaId,ObjectName,TableName,RecordId,Changes(JSON with old/new values),IpAddress,Timestamp, ...
- Fields:
Note: Relationships between tables are defined by Reference EDT in fields. Foreign Keys are NOT created in the database - all data access is through the system's API/OData interface.
All metadata objects belong to a Model (ModelId field). Models provide:
- Functional separation — business logic organized by domain (Core, Tax, CRM, WHS, Custom)
- Security boundaries — client models cannot modify core models
- Namespace — prevents naming conflicts between models
- Extensibility — client models can extend core models via inheritance
🏗️ Models#
MetaModel represents a business logic model — a functional unit that groups related metadata objects.
Core Models (System-Provided)#
Built-in models that come with the system:
-
Core — core business logic (base classes, common functionality)
- Base document types
- Core dictionaries
- System registers
- Cannot be modified by clients (read-only)
-
Tax — tax calculation and reporting model
- Tax-related documents
- Tax registers
- Tax calculation logic
-
CRM — customer relationship management model
- Customer dictionaries
- Sales documents
- Customer-related registers
-
WHS (Warehouse) — warehouse and inventory management model
- Warehouse dictionaries
- Inventory documents
- Stock registers
Custom Models (Client-Created)#
Client models created by customers for their specific needs:
- Custom extensions — client-specific business logic
- Can extend core models — inherit from Core, Tax, CRM, WHS objects
- Can create extensions — override methods, add events, extend dictionaries
- Cannot modify core models — read-only access to system models
- Isolated namespace — own objects, scripts, extensions
Model Structure#
MetaModel Table:
MetaModel:
Id | Name | ModelType | BaseModelId | LayerId | IsSystem | IsReadOnly | Description
1 | Core | System | NULL | 1 | true | true | Core business logic (System layer)
2 | Tax | System | 1 | 1 | true | true | Tax model (System layer)
3 | CRM | System | 1 | 1 | true | true | CRM model (System layer)
4 | WHS | System | 1 | 1 | true | true | Warehouse model (System layer)
5 | ISVRoga | Client | 1 | 2 | false | false | ISV Roga extensions (ISV layer)
6 | Custom | Client | 1 | 3 | false | false | Client customizations (Customer layer)
Layer Assignment:
- System Layer (LayerId=1) — core system models (Core, Tax, CRM, WHS)
- ISV Layer (LayerId=2) — ISV (Independent Software Vendor) models
- Customer Layer (LayerId=3) — customer-specific models
Model Inheritance:
BaseModelId— optional reference to parent model- Client models can extend system models
- Inherits objects, dictionaries, documents from base model
- Can override events, create extensions
Object Model Association#
All metadata objects have ModelId field:
MetaProject: Id, Name, Description, Version, Author, Created, Modified, ...
MetaProjectObject: MetaId (Guid, PK, auto-generated), ProjectMetaId, ObjectType, ObjectMetaId
-- ObjectType: "Dictionary", "Document", "Register", "Script", "DataEntity", "Service"
-- ObjectMetaId: references specific object MetaId (e.g., MetaDictionary.MetaId or MetaScript.MetaId)
-- For extensions: ObjectType="Script", ObjectMetaId=MetaScript.MetaId where ScriptType="ExtensionClass"
MetaDictionary: MetaId (Guid, PK, auto-generated), Name, ModelId, LayerId, IsExtension, BaseDictionaryMetaId, ...
MetaDocumentType: MetaId (Guid, PK, auto-generated), Name, ModelId, LayerId, IsExtension, BaseDocumentTypeMetaId, ...
MetaRegister: MetaId (Guid, PK, auto-generated), Name, ModelId, LayerId, IsExtension, BaseRegisterMetaId, RegisterEngineType, ...
MetaDataEntity: MetaId (Guid, PK, auto-generated), Name, ModelId, LayerId, IsExtension, BaseDataEntityMetaId, ...
MetaEnum: MetaId (Guid, PK, auto-generated), Name, ModelId, LayerId, IsExtension, BaseEnumMetaId, ...
MetaEDT: MetaId (Guid, PK, auto-generated), Name, EDTType, Length, Precision, ...
MetaScript: MetaId (Guid, PK, auto-generated), Name, ModelId, ScriptType, ObjectType, ObjectName, ObjectMetaId (Guid, references metadata object MetaId), EventName, BaseClassName, Code, ...
-- All metadata tables use MetaId (Guid) as primary key, auto-generated using Guid.NewGuid() when creating new record
-- ScriptType: "EventHandler", "ExtensionClass", "DataEntityScript", "JobScript", "RegisterEngineScript", "UnitTest", "IntegrationTest"
-- ObjectType: "Dictionary", "Document", "Register", "DataEntity", "Extension" (for ExtensionClass), "Job" (for JobScript), etc.
-- ObjectName: object name (for EventHandler: "Customer", "Invoice"; for extensions: "InvoicePostingExt")
-- ObjectMetaId: MetaId (Guid) of the metadata object (allows finding object from script)
-- EventName: null (for EventHandler - all events in one class), event-specific for other types
-- BaseClassName: base class name (for EventHandler: "DictionaryEventHandler", "DocumentEventHandler", "RegisterEventHandler"; for ExtensionClass: "InvoicePosting")
-- Code: full C# code
Tests: Stored in MetaScript table with ScriptType="UnitTest" or "IntegrationTest", ObjectType="Test", ObjectMetaId references object under test, Code contains test C# code
MetaProject: MetaId (Guid, PK, auto-generated), Name, Description, Version, Author, Created, Modified, ...
MetaLayer: MetaId (Guid, PK, auto-generated), Name, LayerType, Order, ...
MetaPackage: MetaId (Guid, PK, auto-generated), Name, Version, Description, ...
MetaModel: MetaId (Guid, PK, auto-generated), Name, ModelType, BaseModelMetaId, LayerId, ...
Example:
Core Model (Id=1):
- Dictionary "Customers" (ModelId=1)
- Document "Invoice" (ModelId=1)
- Extension "InvoicePosting" (ModelId=1)
Tax Model (Id=2, BaseModelId=1):
- Document "TaxInvoice" (ModelId=2)
- Register "TaxRegister" (ModelId=2)
- Can reference Core.Customers (ModelId=1)
Custom Model (Id=5, BaseModelId=1):
- Extension "InvoicePostingExt" (ModelId=5, extends Core.InvoicePosting)
- Script "CustomerOnBeforeInsert" (ModelId=5, extends Core.Customer)
- Cannot modify Core objects (Core is read-only)
Security and Permissions#
Model-Level Security:
- System models (
IsSystem=true) — read-only for clients - Client models (
IsSystem=false) — fully editable by clients - Clients cannot create/modify/delete objects in system models
- Clients can create extensions that reference system model objects
- Clients can create new objects in their custom models
Extension Rules:
- Extensions can only be created in client models (not in Core, Tax, CRM, WHS)
- Extensions reference base classes from system models via
BaseModelId - Extensions can override methods, add events, extend dictionaries
Example Extension:
// In Custom Model (ModelId=5)
// Extension extends Core.InvoicePosting (ModelId=1)
public class InvoicePostingExt : InvoicePosting
{
public override void Post()
{
// Custom logic before posting
base.Post(); // Call Core logic
// Custom logic after posting
}
}
Model Package Distribution#
Models can be packaged and distributed:
- Each model can be exported as a package
- Packages include all model objects (dictionaries, documents, registers, scripts)
- Clients can install model packages via Web UI
- Versioning per model (independent version numbers)
Package Structure:
TaxModel-v1.0.0.zip
├── manifest.json
│ └── ModelId: 2, Name: "Tax", Version: "1.0.0"
├── metadata/
│ ├── MetaDictionary.json
│ ├── MetaDocumentType.json
│ └── MetaRegister.json
└── scripts/
└── MetaScript.json
📐 Extended Data Types (EDT)#
MetaEDT defines reusable data types with parameters that control database field generation.
Base EDT Types
Numeric EDT:
-
Int EDT — integer with length parameter (e.g.,
Int32,Int64)- Parameters:
Length(bits: 32, 64) - Database:
INT,BIGINT
- Parameters:
-
Decimal/Float EDT — decimal number with precision and scale
- Parameters:
Length(total digits),Precision(decimal places) - Database:
DECIMAL(length, precision)orNUMERIC(length, precision)
- Parameters:
String EDT:
- String EDT — text field with length
- Parameters:
Length(max characters),IsUnicode,IsFixedLength - Database:
VARCHAR(length),NVARCHAR(length),CHAR(length),NCHAR(length)
- Parameters:
Other EDT:
-
DateTime EDT — date and time with optional timezone
- Parameters:
HasTime(include time component),HasTimezone(include timezone info) - Database:
DATETIME,DATETIME2,TIMESTAMP(depending on database provider) - Examples: Date only, DateTime, DateTime with timezone
- Parameters:
-
Date EDT — date only (without time)
- Parameters:
DateOnly = true - Database:
DATE - Use when time component is not needed
- Parameters:
-
Boolean EDT — true/false values
- Parameters: none (always boolean)
- Database:
BIT(SQL Server),BOOLEAN(PostgreSQL),TINYINT(1)(MySQL) - UI: checkbox control
-
Guid EDT — unique identifiers (GUID/UUID)
- Parameters: none (always 16 bytes / 128 bits)
- Database:
UNIQUEIDENTIFIER(SQL Server),UUID(PostgreSQL),CHAR(36)orBINARY(16)(MySQL) - Use for unique identifiers, especially when distributed systems generate IDs
-
Binary/Blob EDT — binary data storage (files, images, archives)
- Parameters:
MaxSize(max file size in bytes, e.g., 10MB),AllowedExtensions(file type filter, e.g., ".jpg,.png,.zip"),StorageType(Database/FileSystem/CloudStorage) - Database:
VARBINARY(MAX)(SQL Server),BYTEA(PostgreSQL),BLOBorLONGBLOB(MySQL) - Storage options:
- Database storage — store directly in database (VARBINARY/BLOB column)
- FileSystem storage — store in file system, save file path/URL in database (VARCHAR)
- CloudStorage storage — store in cloud storage (Azure Blob, AWS S3), save URL in database (VARCHAR)
- UI: file upload control with drag-and-drop support
- File management:
- Automatic file validation (size, extension)
- Thumbnail generation for images
- File versioning (optional)
- Secure file access with permissions
- Examples: images, documents, archives, videos, audio files
- Parameters:
Enum EDT
Enum EDT — references a specific enumeration (MetaEnum):
-
Parameters:
EnumName— name of the MetaEnum to reference (e.g., "EnumNoYes", "EnumStatus")
-
Automatic behavior:
- When a field uses an Enum EDT, the system automatically:
- Stores integer enum value (0, 1, 2, etc.) in the field
- Creates a dropdown control in UI with enum text labels
- Compiles enum type for type-safe code access
- Runtime provides enum value → text conversion
- When a field uses an Enum EDT, the system automatically:
-
Database mapping:
- Field stores
intvalue (enum underlying type) - UI displays text label from
MetaEnumValue
- Field stores
-
Code generation:
- Enum type compiled as standard C# enum
- Field typed as enum (not int) for type safety
- Example:
customer.IsActive == EnumNoYes.Yes
Example:
1. Create MetaEnum "EnumNoYes" with values: No=0, Yes=1
2. Create EDT "YesNo" with EDTType="Enum", EnumName="EnumNoYes"
3. Add field "IsActive" to dictionary "Customers" using EDT "YesNo"
→ Database: Customers.IsActive (int, stores 0 or 1)
→ UI: Dropdown with "No" and "Yes" options
→ Code: customer.IsActive (type: EnumNoYes)
Reference EDT
Reference EDT — references a specific dictionary:
-
Parameters:
ReferenceTableName— name of the dictionary to referenceReferenceFieldName— field to display in dropdown (usually "Name" or "Code")
-
Automatic behavior:
- When a field uses a Reference EDT, the system automatically:
- Stores reference
Idvalue in the field (no Foreign Key constraint in database) - Creates a dropdown control in UI with reference data
- Relationship logic is handled by the system, not by database constraints
- No additional configuration needed - relationship is defined by the EDT in the field
- Stores reference
- When a field uses a Reference EDT, the system automatically:
Example:
1. Create dictionary "Addresses" (MetaDictionary)
2. Create EDT "Address" with type="Reference", ReferenceTableName="Addresses"
3. Create dictionary "Customers" (MetaDictionary)
4. Add field "Address" to "Customers" using EDT "Address"
→ Automatically creates:
- Column: Customers.AddressId (stores Addresses.Id - business record Id, not MetaId)
- Dropdown in UI with list of addresses
- No Foreign Key constraint in database (handled by application)
Note: MetaId is used only for metadata objects (MetaDictionary, MetaDocumentType, etc.), not for business data records
📚 Dictionaries#
MetaDictionary represents a dictionary - a simple lookup table (e.g., "Addresses", "Customers", "Products").
Important distinction:
MetaDictionary(metadata object) — usesMetaId(Guid, PK) as primary key- Business table (
Customers,Addresses, etc.) — usesId(PK) as primary key for business records - When creating field reference: business table column stores
Idof referenced business record, notMetaId
Properties:
- Simple table structure
- Can be referenced by other objects via Reference EDT
- Supports hierarchical structure (parent-child)
- Runtime business table structure:
Id(PK),Code,Name,IsActive, etc.- Example:
Customerstable hasId(PK),Code,Name,AddressId(references Addresses.Id)
- Example:
- Belongs to a Model (ModelId)
- Belongs to a Layer (LayerId) - System, ISV, or Customer
Extension Support:
- IsExtension — boolean flag indicating if this dictionary extends another
- BaseDictionaryMetaId — references
MetaDictionary.MetaIdof base dictionary (ifIsExtension=true) - Extension creates new dictionary record with
IsExtension=trueandBaseDictionaryMetaIdpointing to base - Extension can add new fields (via MetaDictionaryProperty)
- Extension can override events and add new scripts (via MetaScript, linked to extension object)
- Supports extension chains: Base → ISV Extension → Customer Extension
Field Management:
- Fields are edited directly in dictionary editor (not separate page)
- Fields stored in
MetaDictionaryPropertytable - Extension shows inherited fields (read-only) + extension fields (editable)
Usage:
- Master data storage
- Dropdown source for Reference EDT fields
- Simple catalogs and directories
📋 Enumerations (Enums)#
MetaEnum represents enumeration definitions - simple lists with text and integer value pairs that compile into C# enums.
Properties:
- Simple key-value pairs: text label → integer value
- Compiles to standard C# enum type
- Can be used in field definitions via Enum EDT
- Supports extension (add values to existing enum)
Metadata Structure:
MetaEnumtable — stores enum definitionsMetaEnumValuetable — stores enum values (text-value pairs)
MetaEnum Table Structure:
MetaEnum:
MetaId (Guid, PK) | Name | ModelId | LayerId | IsExtension | BaseEnumMetaId | Description
GUID_ENUM_1 | EnumNoYes | 1 | 1 | false | NULL | Simple Yes/No enumeration
GUID_ENUM_2 | EnumStatus | 1 | 1 | false | NULL | Document status enumeration
GUID_ENUM_3 | EnumStatusExt | 5 | 2 | true | GUID_ENUM_2 | Extension of EnumStatus (adds new values)
MetaEnumValue Table Structure:
MetaEnumValue:
MetaId (Guid, PK) | EnumMetaId | Name | Value | DisplayOrder | Description
GUID_VAL_1 | GUID_ENUM_1 | No | 0 | 1 | No value
GUID_VAL_2 | GUID_ENUM_1 | Yes | 1 | 2 | Yes value
GUID_VAL_3 | GUID_ENUM_2 | Draft | 0 | 1 | Draft status
GUID_VAL_4 | GUID_ENUM_2 | Approved | 1 | 2 | Approved status
GUID_VAL_5 | GUID_ENUM_2 | Posted | 2 | 3 | Posted status
GUID_VAL_6 | GUID_ENUM_3 | Cancelled | 3 | 4 | Cancelled status (added in extension)
Example: EnumNoYes
- Name: "No", Value: 0
- Name: "Yes", Value: 1
Compilation to C# Enum:
When runtime types are generated, MetaEnum compiles to standard C# enum:
// Generated from MetaEnum with Name="EnumNoYes"
public enum EnumNoYes
{
No = 0,
Yes = 1
}
// Generated from MetaEnum with Name="EnumStatus" (with extension)
public enum EnumStatus
{
Draft = 0,
Approved = 1,
Posted = 2,
Cancelled = 3 // Added from extension EnumStatusExt
}
Extension Support:
IsExtension=true— marks enum as extension of another enumBaseEnumId— references base enum- Extension enum values are merged into base enum during compilation
- Values must not conflict (same value = same name, or different value)
- Extension values are appended to base enum
Usage in Fields:
- Field with Enum EDT references
MetaEnumby name - Field stores integer value (0, 1, 2, etc.)
- UI automatically renders dropdown with enum text labels
- Runtime provides type-safe enum access
Example Usage:
1. Create MetaEnum "EnumNoYes" with values: No=0, Yes=1
2. Create EDT "YesNo" with EDTType="Enum", EnumName="EnumNoYes"
3. Add field "IsActive" to dictionary "Customers" using EDT "YesNo"
→ Field stores: 0 (No) or 1 (Yes)
→ UI shows: Dropdown with "No" and "Yes" options
→ Code access: customer.IsActive == EnumNoYes.Yes
📄 Documents#
MetaDocumentType represents a document type definition with header and lines structure (like invoices, orders, etc.).
Metadata Structure:
-
MetaDocumentTypetable — stores document type definitions- Fields:
MetaId(Guid, PK),Name,Description,NumberingSequence, etc.
- Fields:
-
MetaDocumentHeaderFieldtable — stores header field definitions for each document type (defines fields that will exist in document header records, not the header records themselves)- Fields:
MetaId(Guid, PK),DocumentTypeMetaId(references MetaDocumentType.MetaId),FieldName,EDTMetaId,IsRequired, etc. - Separate table for header field definitions (not mixed with line field definitions)
- Extension support: When document type is extended, new header field definitions are added to this table with
DocumentTypeMetaIdpointing to extension document type
- Fields:
-
MetaDocumentLinetable — stores document line type definitions (defines line types for documents)- Fields:
MetaId(Guid, PK),DocumentTypeMetaId(references MetaDocumentType.MetaId),Name,Description,ModelId,LayerId,IsExtension,BaseDocumentLineMetaId, etc. - Automatic generation: When document type is created, system automatically generates default line type with name "{DocumentTypeName}Line" (e.g., "SalesOrderLine" for "SalesOrder" document)
- Multiple line types: One document type can have multiple line types (e.g., InvoiceLine, InvoiceDiscountLine, InvoiceTaxLine)
- C# class generation: System automatically generates C# class for each line type in metadata with properties:
- DocumentType (references document type)
- LineNumber (int)
- Line fields (generated from MetaDocumentLineField)
- Extension support:
IsExtension=true,BaseDocumentLineMetaIdreferences base line type MetaId
- Fields:
-
MetaDocumentLineFieldtable — stores field definitions for document lines (defines fields that will exist in document line records, not the line records themselves)- Fields:
MetaId(Guid, PK),DocumentLineMetaId(references MetaDocumentLine.MetaId),FieldName,EDTMetaId,IsRequired, etc. - Used for line field definitions (header fields use
MetaDocumentHeaderFieldtable) - Extension support: When document line type is extended, new line field definitions are added to this table with
DocumentLineMetaIdpointing to extension line type
- Fields:
-
MetaDocumentStatustable — stores status definitions for document types (workflow states)- Fields:
MetaId(Guid, PK),DocumentTypeMetaId(references MetaDocumentType.MetaId),StatusName,StatusValue,DisplayOrder,Description,IsInitial,IsFinal,WorkflowOrder, etc. - Defines possible statuses for each document type (e.g., SalesOrder: "Draft", "Confirmed", "Shipped", "Completed")
- Workflow defines status transition order
- One document type can have multiple statuses
- Example: Inventory document can have statuses: "Draft", "Started", "Completed", "Closed"
- Fields:
Runtime Structure (Generated Tables): For each document type, system generates runtime tables for business data:
-
Header table (
{DocumentName}) — main document data (business records)- Fields generated from
MetaDocumentHeaderFieldrecords - Standard fields:
Id(PK),Number,Date,Status, etc. - Reference fields:
CustomerId(references Customers.Id), etc. - Note: Business tables use
Id, notMetaId(MetaId is only for metadata objects) - Automatic C# class generation: System generates C# class for document type in metadata with properties: DocumentType, Status, Number, Date, and all header fields
- Fields generated from
-
Lines table(s) (
{LineTypeName}) — document lines/rows (business records)- One table per line type (if document has multiple line types, multiple tables are generated)
- Default line type generates table
{DocumentName}Lines(e.g., "SalesOrderLines" for "SalesOrder" document with default line type "SalesOrderLine") - Additional line types generate separate tables (e.g., "InvoiceDiscountLines", "InvoiceTaxLines")
- Fields generated from
MetaDocumentLineFieldrecords (filtered by DocumentLineMetaId) - Standard fields:
Id(PK),DocumentId(references header.Id),LineNumber, etc. - Reference fields:
ProductId(references Products.Id), etc. - Automatic C# class generation: System generates C# class for each line type in metadata with properties: DocumentType, LineNumber, and all line fields
Properties:
- One-to-many relationship: Header → Lines (automatic in runtime)
- Document posting workflows
- Status management — statuses defined in
MetaDocumentStatustable (workflow states) - Numbering sequences
- Belongs to a Model (ModelId)
- Belongs to a Layer (LayerId) - System, ISV, or Customer
Extension Support:
- IsExtension — boolean flag indicating if this document type extends another
- BaseDocumentTypeMetaId — references
MetaDocumentType.MetaIdof base document type (ifIsExtension=true) - Extension creates new document type record with
IsExtension=trueandBaseDocumentTypeMetaIdpointing to base - Extension can add new header fields (via MetaDocumentHeaderField)
- Extension can add new line types (via MetaDocumentLine) or extend existing line types
- Extension can add new line fields to existing line types (via MetaDocumentLineField)
- Extension can add new statuses (via MetaDocumentStatus)
- Extension can override events and add new scripts (via MetaScript, linked to extension object)
- Supports extension chains: Base → ISV Extension → Customer Extension
Field Management:
- Header fields are edited directly in document editor (not separate page)
- Line types are managed in Developer Studio (separate form for DocumentLine editing)
- Line fields are edited in line type editor (not document editor)
- Header fields stored in
MetaDocumentHeaderFieldtable - Line types stored in
MetaDocumentLinetable - Line fields stored in
MetaDocumentLineFieldtable (references MetaDocumentLine.MetaId) - Statuses stored in
MetaDocumentStatustable - Extension shows inherited fields (read-only) + extension fields (editable)
- Automatic class generation: When document type is created, system automatically generates C# class in metadata with properties: DocumentType, Status, and all header fields
Status Management (Workflow):
MetaDocumentStatustable — defines possible statuses for each document type- Workflow order —
WorkflowOrderfield defines status transition sequence - Initial status —
IsInitial=trueflag marks the status document starts with (typically "Draft") - Final status —
IsFinal=trueflag marks terminal statuses (workflow ends here, e.g., "Completed", "Cancelled") - Status transitions — documents can move from one status to another following
WorkflowOrder - Status dropdown — UI automatically generates status dropdown from
MetaDocumentStatusrecords - Status validation — system validates status transitions based on workflow order
- Status colors/icons — UI can display statuses with colors and icons for visual indication
Status Examples:
- Invoice: "Draft" (IsInitial) → "Posted" → "Cancelled" (IsFinal)
- SalesOrder: "Draft" (IsInitial) → "Confirmed" → "Shipped" → "Invoiced" → "Completed" (IsFinal)
- Inventory: "Draft" (IsInitial) → "Started" → "Completed" → "Closed" (IsFinal)
Initial SUBTYPE — required whenever a type declares subtypes#
MetaDocumentSubtype carries its own IsInitial (MIQS "Initial subtype", one per
type). It is not decoration: a document type that declares subtypes MUST mark one of
them initial.
IDocumentManager enforces it. Both creation doors — typed NewDocumentAsync<T> and
untyped CreateDocumentAsync — resolve an unspecified subtype through
ResolveInitialSubtypeAsync:
| Case | Result |
|---|---|
| Subtype passed by the caller | used as given |
Not passed, type has an IsInitial subtype |
the initial subtype is applied |
| Not passed, type declares subtypes but none is initial | InvalidOperationException, naming the type and its subtypes |
| Type declares no subtypes at all | null — the type simply does not use subtypes |
The exception is deliberate, because Subtype = NULL is not a neutral value:
DocumentPostingService.BuildChainAsyncscopes the transaction chain by subtype. A null subtype narrows to nothing and pulls in chains belonging to other states — on apostOnSavetype, a document the user considers a draft posts itself into the ledger the moment it is saved.- Subtype transitions are validated against the declared transition table. A document
with no subtype has nothing to transition from, so the table is not enforced and
callers silently skip the workflow (
Draft → Receivedon a type that declaresDraft → Ordered → Received).
Both failure modes are invisible until they reach the register, which is why the check fires at creation instead.
Example:
MetaDocumentType "Invoice":
- MetaId=GUID_INVOICE, Name="Invoice"
MetaDocumentHeaderField (for Invoice):
- DocumentTypeMetaId=GUID_INVOICE, FieldName="CustomerMetaId", EDTMetaId="Reference(Customers)", ...
- DocumentTypeMetaId=GUID_INVOICE, FieldName="Date", EDTMetaId="DateTime", ...
MetaDocumentLine (for Invoice):
- MetaId=GUID_INVOICE_LINE, DocumentTypeMetaId=GUID_INVOICE, Name="InvoiceLine", Description="Invoice line items"
- System automatically generates C# class "InvoiceLine" in metadata
MetaDocumentLineField (for InvoiceLine):
- DocumentLineMetaId=GUID_INVOICE_LINE, FieldName="ProductMetaId", EDTMetaId="Reference(Products)", ...
- DocumentLineMetaId=GUID_INVOICE_LINE, FieldName="Quantity", EDTMetaId="Decimal(18,2)", ...
- DocumentLineMetaId=GUID_INVOICE_LINE, FieldName="Price", EDTMetaId="Decimal(18,2)", ...
MetaDocumentStatus (for Invoice):
- DocumentTypeMetaId=GUID_INVOICE, StatusName="Draft", StatusValue="Draft", IsInitial=true, WorkflowOrder=1
- DocumentTypeMetaId=GUID_INVOICE, StatusName="Posted", StatusValue="Posted", IsFinal=false, WorkflowOrder=2
- DocumentTypeMetaId=GUID_INVOICE, StatusName="Cancelled", StatusValue="Cancelled", IsFinal=true, WorkflowOrder=3
Runtime tables generated (business data tables):
Invoices (Header):
- Id (PK), Number, Date, CustomerId (references Customers.Id), TotalAmount, Status
- System generates C# class "Invoice" in metadata with properties: DocumentType, Status, Number, Date, CustomerId, TotalAmount, etc.
InvoiceLines (Lines, from default MetaDocumentLine "InvoiceLine"):
- Id (PK), InvoiceId (references Invoices.Id), LineNumber, ProductId (references Products.Id), Quantity, Price, Amount
- System generates C# class "InvoiceLine" in metadata with properties: DocumentType, LineNumber, ProductId, Quantity, Price, Amount, etc.
Note: Business data tables use Id (not MetaId). MetaId is only for metadata objects.
📊 Registers#
MetaRegister represents register type definitions with header and lines structure, register engine type, and custom behavior configuration.
Key Concept: Registers have a Register Engine — a customizable calculation engine that defines how register data is accumulated, balanced, and processed. Different engines provide different behaviors (FIFO, financial collapse, flat history, etc.).
Metadata Structure:
-
MetaRegistertable — stores register type definitions and engine configuration- Fields:
MetaId(Guid, PK),Name,Description,RegisterEngineType,EngineScriptMetaId(optional custom engine),ModelId,LayerId,IsExtension,BaseRegisterMetaId, etc. - RegisterEngineType — determines which engine to use (Standard, FIFO, Financial, FlatHistory, Custom)
- EngineScriptMetaId — optional reference to
MetaScript.MetaIdfor custom engine behavior - Belongs to a Model (ModelId)
- Belongs to a Layer (LayerId)
- Extension support:
IsExtension=true,BaseRegisterMetaIdreferences base register MetaId
- Fields:
-
MetaRegisterHeadertable — stores header field definitions for each register type (defines fields in register header, not the header record itself)- Fields:
MetaId(Guid, PK),RegisterMetaId(references MetaRegister.MetaId),FieldName,EDTMetaId,IsRequired, etc. - Separate table for header field definitions (not mixed with line field definitions)
- Field definitions edited directly in register editor (not separate page)
- Fields:
-
MetaRegisterFieldtable — stores field definitions for register lines (defines fields in register lines, not the line records themselves)- Fields:
MetaId(Guid, PK),RegisterMetaId(references MetaRegister.MetaId),FieldName,FieldType("Line" for line fields),EDTMetaId,IsRequired, etc. - Used for line field definitions (header fields use
MetaRegisterHeadertable) - Field definitions edited directly in register editor (not separate page)
- Fields:
Extension Support:
- IsExtension — boolean flag indicating if this register extends another
- BaseRegisterMetaId — references
MetaRegister.MetaIdof base register (ifIsExtension=true) - Extension creates new register record with
IsExtension=trueandBaseRegisterMetaIdpointing to base - Extension can add new header/line fields (via MetaRegisterHeader/MetaRegisterField)
- Extension can override events and add new scripts (via MetaScript, linked to extension object)
- Extension shows inherited fields (read-only) + extension fields (editable)
- Supports extension chains across layers
Runtime Structure (Generated Tables): For each register type, system generates two runtime tables for business data:
-
Header table (
{RegisterName}) — main register data (business records)- Fields generated from
MetaRegisterHeaderrecords - Standard fields:
Id(PK),Period,Dimension1,Dimension2, etc. - Note: Business tables use
Id, notMetaId(MetaId is only for metadata objects)
- Fields generated from
-
Lines table (
{RegisterName}Lines) — register lines/rows (business records)- Fields generated from
MetaRegisterFieldrecords (where FieldType="Line") - Standard fields:
Id(PK),RegisterId(references header.Id),Resource,Quantity, etc.
- Fields generated from
🔧 Register Engine#
Register Engine is a customizable calculation engine that defines how register data behaves. Base engine provides standard functionality, but developers can customize it or create custom engines.
Built-in Register Engines#
1. Standard Engine (default)
Standard accumulation register behavior:
- Accumulates values (sum, count, etc.)
- Can calculate balances at any point in time
- Supports standard operations: GetBalance, GetTurnover, GetRemains
Use cases:
- Product balances in warehouses
- Account balances
- Standard accumulation tracking
Example:
MetaRegister "ProductBalance":
- RegisterEngineType = "Standard"
- Accumulates Quantity, Amount by Product, Warehouse, Period
2. FIFO Engine (First In, First Out)
FIFO calculation engine:
- Maintains FIFO queue of receipts and issues
- When issuing, uses oldest receipts first
- Automatically tracks cost basis using FIFO method
Use cases:
- Inventory valuation (FIFO cost method)
- Stock cost calculation
- Warehouse management with FIFO cost tracking
Example:
MetaRegister "FIFOInventory":
- RegisterEngineType = "FIFO"
- Receipts stored in queue order
- Issues consume from oldest receipts first
- Automatically calculates cost using FIFO
How it works:
- Receipt document posts → adds to FIFO queue (with cost, quantity, date)
- Issue document posts → consumes from queue (oldest first)
- Balance query → calculates remaining queue items
- Cost query → uses FIFO cost basis
3. Financial Engine (Collapsing by Amount)
Financial register that automatically collapses to zero:
- Accumulates by dimensions (Account, Counterparty, etc.)
- Automatically collapses when Debit = Credit (balance = 0)
- Removes zero-balance records automatically
- Optimizes storage by keeping only non-zero balances
Use cases:
- Accounting registers (accounts, sub-accounts)
- Financial transactions
- Reconciliation registers
Example:
MetaRegister "AccountRegister":
- RegisterEngineType = "Financial"
- Dimensions: Account, Counterparty, Currency
- Automatically collapses when Debit = Credit
- Only stores non-zero balances
How it works:
- Transaction posts → adds Debit and Credit
- After posting → engine checks balance (Debit - Credit)
- If balance = 0 → automatically removes record
- Query → returns only active (non-zero) balances
4. FlatHistory Engine (Simple History)
Flat history register — just stores historical records:
- No accumulation logic
- No balance calculation
- Simply records all changes as-is
- Used for audit trails and history
Use cases:
- Change history
- Audit logs
- Event tracking
- Simple historical records
Example:
MetaRegister "CustomerHistory":
- RegisterEngineType = "FlatHistory"
- Stores all changes: what, when, who, old value, new value
- No accumulation, just history records
How it works:
- Any change → creates history record
- Stores timestamp, user, old value, new value
- Query → returns all history records (no aggregation)
5. Custom Engine
Custom engine with developer-defined behavior:
EngineScriptIdreferencesMetaScriptwith custom engine code- Developer writes custom calculation logic
- Full control over accumulation, balancing, querying
Use cases:
- Custom business rules
- Industry-specific calculations
- Complex accumulation logic
Example:
MetaRegister "CustomWarehouse":
- RegisterEngineType = "Custom"
- EngineScriptId = 123 (references MetaScript with custom engine code)
MetaScript (Id=123):
- ScriptType = "RegisterEngineScript"
- Code = [custom engine implementation]
Register Engine Architecture#
Engine Interface:
public interface IRegisterEngine
{
// Initialize register with configuration
void Initialize(MetaRegister register, MetaRegisterHeader[] headers, MetaRegisterField[] lines);
// Process document posting (movements)
void Post(Document document, RegisterMovement[] movements);
// Get balance at specific point in time
RegisterBalance GetBalance(DateTime date, RegisterDimension[] dimensions);
// Get turnover for period
RegisterTurnover GetTurnover(DateTime fromDate, DateTime toDate, RegisterDimension[] dimensions);
// Get remains (current balance)
RegisterRemains GetRemains(RegisterDimension[] dimensions);
// Custom query (engine-specific)
object Query(RegisterQuery query);
}
Engine Implementation:
Each engine type implements IRegisterEngine:
StandardRegisterEngine— standard implementationFIFORegisterEngine— FIFO implementationFinancialRegisterEngine— financial collapsing implementationFlatHistoryRegisterEngine— flat history implementationCustomRegisterEngine— dynamically compiled from MetaScript
Engine Registration:
// In MetaRegister table:
RegisterEngineType = "FIFO" // Uses built-in FIFORegisterEngine
// OR
RegisterEngineType = "Custom"
EngineScriptId = 123 // Uses compiled script as engine
Register Engine Examples#
Standard Balance Register:
MetaRegister "ProductStock":
- RegisterEngineType = "Standard"
- Header: Warehouse, Period
- Lines: Product, Quantity, Amount
Query: GetBalance(Warehouse="W1", Period="2025-01-31")
→ Returns: Product balances at end of January
FIFO Register:
MetaRegister "InventoryFIFO":
- RegisterEngineType = "FIFO"
- Header: Warehouse
- Lines: Product, ReceiptDate, Quantity, Cost
Receipt: +100 pcs @ $10 each (2025-01-01)
Receipt: +50 pcs @ $12 each (2025-01-15)
Issue: -60 pcs (2025-01-20)
→ FIFO consumes: 60 from first receipt @ $10 = $600 cost
→ Remaining: 40 @ $10 + 50 @ $12
Financial Register:
MetaRegister "AccountTurnover":
- RegisterEngineType = "Financial"
- Header: Account, Counterparty
- Lines: Debit, Credit, Currency
Transaction 1: Account="1010", Debit=1000, Credit=0
Transaction 2: Account="1010", Debit=0, Credit=500
Transaction 3: Account="1010", Debit=0, Credit=500
→ Balance = 0 → Record automatically collapsed/removed
Usage:
- Balance tracking (Standard, FIFO)
- Financial accounting (Financial engine with auto-collapse)
- History tracking (FlatHistory)
- Custom business logic (Custom engine via scripts)
🔧 Services (Service Classes)#
MetaService represents service class definitions — helper classes for business logic with no visual interface.
Concept: Services are C# classes in free form that can be created in metadata and used throughout business logic. They are pure code classes (no tables, no forms), compiled into regular C# classes that can be used in events, extensions, and other services.
Properties:
- No visual interface — services are pure code classes, no UI/forms/tables
- C# classes in free form — can define any fields, properties, and methods
- Metadata stored in
MetaService— name, description, namespace, model - Code stored in
MetaScript— withScriptType="ServiceClass",ObjectType="Service" - Can be used in business logic — events, extensions, other services, jobs
- Can be debugged and compiled — full development experience in Web IDE
- Compiled to regular C# classes — available at runtime for use in business logic
Metadata Structure:
-
MetaServicetable — stores service definitions- Fields:
MetaId(Guid, PK),Name,Description,ModelId,LayerId,Namespace,Author,Modified,Version
- Fields:
-
MetaScripttable — stores service class code- Fields:
MetaId(Guid, PK),ScriptType="ServiceClass",ObjectType="Service",ObjectName(service name),ObjectMetaId(references MetaService.MetaId),Code(full C# class code),ModelId, ...
- Fields:
Example Service Definition:
MetaService:
MetaId: GUID_LOC_HELPER
Name: LocationHelper
Description: Helper for address calculations and location services
ModelId: 1 (Core)
LayerId: 1 (System)
Namespace: ZuloOne.Services
MetaScript (for LocationHelper):
MetaId: GUID_SCRIPT_LOC
ScriptType: ServiceClass
ObjectType: Service
ObjectName: LocationHelper
ObjectMetaId: GUID_LOC_HELPER
Code:
public class LocationHelper
{
public static double CalculateDistance(double lat1, double lon1, double lat2, double lon2)
{
// Haversine formula implementation
...
}
public static bool IsAddressValid(string address)
{
// Address validation logic
...
}
public static Address ParseAddress(string fullAddress)
{
// Address parsing logic
...
}
}
Compilation and Runtime:
- Service classes are compiled as regular C# classes using Roslyn
- Compiled into assemblies and loaded via
AssemblyLoadContext - Available at runtime with full namespace support
- Can be instantiated or used as static utility classes
Usage in Business Logic:
Services can be used in:
- Event handlers:
public class CustomerEventHandler : DictionaryEventHandler
{
public override void OnBeforeInsert(dynamic entity)
{
// Use service in event handler
if (!LocationHelper.IsAddressValid(entity.Address))
throw new ValidationException("Invalid address format");
var distance = LocationHelper.CalculateDistance(...);
}
}
- Extension classes:
public class InvoicePostingExt : InvoicePosting
{
public override void Post()
{
base.Post();
// Use service in extension
var helper = new LocationHelper();
var deliveryDistance = LocationHelper.CalculateDistance(...);
}
}
- Other services:
public class ShippingService
{
public void CalculateShipping(dynamic order)
{
var distance = LocationHelper.CalculateDistance(...);
// Use LocationHelper in another service
}
}
- Job scripts:
public class ScheduledJob
{
public void Run()
{
// Use services in scheduled jobs
var locations = LocationHelper.GetNearbyLocations(...);
}
}
Development in Web IDE:
- Create service via Developer Studio
- Write C# class code with fields and methods
- Compile and test service
- Debug service with breakpoints
- View compilation errors and warnings
- Full IntelliSense support (Monaco Editor + Roslyn LSP)
No Visual Interface: Unlike Dictionaries, Documents, and Registers, Services do NOT have:
- Business data tables
- Forms for data entry
- List views
- UI components
Services are pure code classes — compile to C# classes that can be used programmatically.
🌐 Data Entities (OData resources)#
MetaDataEntity is a public OData 4 collection at /odata/{PublicCollectionName}:
a designed projection of dictionary/document tables (joins, field mapping, optional
computed fields in C#). This is not a web service (POST /api/rest/{Name} is RPC).
Properties:
- PublicCollectionName — entity-set name; empty means the object
Name - IsActive — false answers 404, same as an unknown collection
- IsReadOnly — POST/PATCH/DELETE return 405
- RequireExplicitRoles — empty permission matrix is otherwise public; with the flag at least one grant is required
- Data sources —
MetaDataEntityDataSource: one primary Dictionary/Document plus optional Inner/Outer joins - Fields —
MetaDataEntityField: mapped column orIsComputed(filled byMapFromSource) - Events —
TypedDataEntityEventHandler<T>(DataEntityEventsscript)
OData host (DataEntityHostController):
/odata— service document/odata/$metadata— EDMX 4.0/odata/{collection}?$filter=&$select=&$orderby=&$top=&$skip=&$count=/odata/{collection}({key})— GET/PATCH/DELETE- POST to the collection inserts the primary source row through
IDataService
Auth: SPA HS256 unchanged. A login.getzulo.com client_credentials token with
aud = ZuloOne:PublicUrl maps email → existing MetaUser. The same Bearer
works for /odata, /api/rest, and /api/data. RLS predicates on source tables apply.
Example:
GET /odata/CustomersV2?$filter=ID eq 'C-100'&$select=ID,Name&$top=20
Authorization: Bearer …
Designer: Developer → Scripting → Data entities (/dataentities).
In-process (events, commands, services, tests): GetService<IDataEntityManager>() —
same mapping, write hooks and source RLS; RequireExplicitRoles is HTTP-only.
Wiki: wiki/developer/data-entities.md.
📋 Dictionary Fields (MetaDictionaryProperty)#
MetaDictionaryProperty defines fields specifically for dictionaries.
Structure:
- Separate table for dictionary field definitions (like MetaDocumentHeaderField/MetaDocumentLineField for documents, MetaRegisterHeader/MetaRegisterField for registers)
- Each field definition belongs to a dictionary via
DictionaryMetaId(references MetaDictionary.MetaId)
Properties:
FieldName— field nameEDTMetaId— reference toMetaEDT.MetaId(defines type and parameters)IsRequired— mandatory fieldIsUnique— unique constraintDefaultValue— default valueValidationRules— custom validation
Database generation:
- Field type, length, precision are determined by the EDT
- Constraints are generated based on field properties
- Fields generated into single dictionary table (unlike documents/registers which have header+lines)
Note: Documents use MetaDocumentHeaderField and MetaDocumentLineField for fields, and MetaDocumentStatus for workflow statuses. Registers use MetaRegisterHeader and MetaRegisterField for fields. Each object type has its own field structure tables.
Field Types (Legacy / Direct)#
Fields can also use direct types without EDT (for simple cases):
String— text fieldsInt— integer numbersDecimal— decimal numbersDateTime— date and timeBoolean— true/false valuesReference— foreign key (now via Reference EDT)Enum— enumeration referenceCollection— one-to-many collectionComputed— calculated fields (read-only)
📜 Scripts Storage (MetaScript)#
MetaScript is a separate table for storing all C# code (scripts, classes, processing logic).
Why separate table:
- Separates code storage from metadata definitions
- Allows different types of scripts (events, data processing, extensions)
- Enables better versioning and management
- Single source of truth for all script code
MetaScript Table Structure:
MetaScript:
MetaId (Guid, PK) | Name | ScriptType | ObjectType | ObjectName | ObjectMetaId | EventName | BaseClassName | Code | ModelId | Author | Modified | Version
GUID1 | CustomerEvents | EventHandler | Dictionary | Customer | GUID_DICT_1 | NULL | DictionaryEventHandler | public class CustomerEventHandler... | 1 | admin | 2025-11-02 | 1
GUID2 | InvoiceEvents | EventHandler | Document | Invoice | GUID_DOC_1 | NULL | DocumentEventHandler | public class InvoiceEventHandler... | 1 | admin | 2025-11-02 | 1
GUID3 | CustomersDataProcessing | DataEntityScript | DataEntity | Customers | GUID_DE_1 | NULL | NULL | return data.Where(...); | 1 | admin | 2025-11-02 | 1
GUID4 | InvoicePostingExt | ExtensionClass | Extension | InvoicePostingExt | NULL | NULL | InvoicePosting | public class InvoicePostingExt... | 5 | admin | 2025-11-02 | 1
GUID5 | LocationHelper | ServiceClass | Service | LocationHelper | GUID_SVC_1 | NULL | NULL | public class LocationHelper { ... } | 1 | admin | 2025-11-02 | 1
MetaService Table Structure:
MetaService:
MetaId (Guid, PK) | Name | Description | ModelId | LayerId | Namespace | Author | Modified | Version
GUID_SVC_1 | LocationHelper | Helper for address calculations and location services | 1 | 1 | ZuloOne.Services | admin | 2025-11-02 | 1
MetaId (Guid) as Primary Key for all metadata objects:
All metadata tables use MetaId of type Guid as primary key (instead of Id):
- Auto-generated when creating new record (using
Guid.NewGuid()) - Unique identifier across all metadata objects
- Used for references and cross-table relationships
- Allows finding metadata object from script using
ObjectMetaIdfield in MetaScript
Examples:
MetaDictionary: MetaId (Guid, PK), Name, ModelId, ...
MetaDocumentType: MetaId (Guid, PK), Name, ModelId, ...
MetaRegister: MetaId (Guid, PK), Name, ModelId, ...
MetaEnum: MetaId (Guid, PK), Name, ModelId, ...
MetaDataEntity: MetaId (Guid, PK), Name, ModelId, ...
MetaDictionaryProperty: MetaId (Guid, PK), DictionaryMetaId (references MetaDictionary.MetaId), ...
Script Types:
EventHandler— event handler class (one per Dictionary/Document/Register, inherits from base event handler class)DataEntityScript— DataEntity processing codeExtensionClass— full extension class codeServiceClass— service class (helper classes for business logic, no visual UI)JobScript— scheduled job codeValidationScript— custom validation logicRegisterEngineScript— custom register engine implementation (when RegisterEngineType = "Custom")UnitTest— unit test scripts (for testing individual functions/events)IntegrationTest— integration test scripts (for testing object interactions)
Metadata Objects Reference Scripts:
MetaDataEntity.ScriptId→ references MetaScript (for processing code, ScriptType="DataEntityScript")MetaDictionary→ automatically has one EventHandler class in MetaScript (ObjectName=dictionary name, ScriptType="EventHandler", BaseClassName="DictionaryEventHandler")MetaDocumentType→ automatically has one EventHandler class in MetaScript (ObjectName=document name, ScriptType="EventHandler", BaseClassName="DocumentEventHandler")MetaRegister→ automatically has one EventHandler class in MetaScript (ObjectName=register name, ScriptType="EventHandler", BaseClassName="RegisterEventHandler")- Business Logic Extensions → stored in MetaScript with ScriptType="ExtensionClass", BaseClassName points to base class
Event Handlers stored in MetaScript:
Each metadata object (Dictionary, Document, Register) has exactly one event handler class stored in MetaScript:
MetaScript.ScriptType = "EventHandler"— identifies as event handler classMetaScript.ObjectType— object type (e.g., "Dictionary", "Document", "Register")MetaScript.ObjectName— object name (e.g., "Customer", "Invoice", "InventoryBalance")MetaScript.ObjectMetaId— MetaId (Guid) of the metadata object (references MetaDictionary.MetaId, MetaDocumentType.MetaId, or MetaRegister.MetaId)MetaScript.BaseClassName— base event handler class:"DictionaryEventHandler"— for dictionaries"DocumentEventHandler"— for documents"RegisterEventHandler"— for registers
MetaScript.Code— full C# class code with all event handler methodsMetaScript.EventName— always NULL for EventHandler (all events in one class)
Querying event handlers:
-- Get event handler class for Customer dictionary by name
SELECT * FROM MetaScript
WHERE ObjectName = 'Customer' AND ScriptType = 'EventHandler' AND ObjectType = 'Dictionary'
-- Get event handler class for Customer dictionary by MetaId
SELECT * FROM MetaScript
WHERE ObjectMetaId = 'GUID_DICT_1' AND ScriptType = 'EventHandler'
-- Find metadata object from script (reverse lookup)
SELECT d.* FROM MetaDictionary d
JOIN MetaScript s ON d.MetaId = s.ObjectMetaId
WHERE s.MetaId = 'GUID_SCRIPT_1'
-- All references use MetaId (Guid) instead of Id
-- Example: MetaDictionaryProperty.DictionaryMetaId references MetaDictionary.MetaId
-- Example: MetaDocumentHeaderField.DocumentTypeMetaId references MetaDocumentType.MetaId
-- Example: MetaDocumentLine.DocumentTypeMetaId references MetaDocumentType.MetaId
-- Example: MetaDocumentLineField.DocumentLineMetaId references MetaDocumentLine.MetaId
-- Example: MetaDocumentStatus.DocumentTypeMetaId references MetaDocumentType.MetaId
-- Example: BaseDictionaryMetaId references MetaDictionary.MetaId (for extensions)
Automatic Generation: When creating a new Dictionary/Document/Register, system automatically creates corresponding event handler class entry in MetaScript with empty method stubs.
No separate MetaEvent table needed — one class per object, all events in one place
Events (Event Handler Classes)#
Events allow custom business logic to be executed at specific points in the entity lifecycle. Each metadata object (Dictionary, Document, Register) has exactly one event handler class that inherits from a base event handler class.
Concept: Event Handler Classes — each object gets one C# class that inherits from a base event handler class (DictionaryEventHandler, DocumentEventHandler, RegisterEventHandler). All event methods are defined in this single class. When creating a new object, system automatically generates empty event handler class.
Event Storage in Database:
MetaScript Table:
Id | Name | ScriptType | ObjectName | EventName | Code | IsActive
1 | CustomerOnBeforeInsert | EventScript | Customer | OnBeforeInsert | if (!Entity.Phone.StartsWith("+")) throw new Exception("Invalid phone"); | true
Code Storage:
- Event handlers stored directly in
MetaScripttable - Fields:
ObjectName,EventName,ScriptType="EventScript",Code - Uses
CSharpScript(Roslyn Scripting API) for runtime compilation - Compiled scripts cached in memory (or Redis) for performance
- No full assembly compilation — lightweight script execution
- No separate MetaEvent table — events are just scripts with ObjectName/EventName
Script Context:
Each event script receives a ScriptContext with:
Entity(dynamic) — the entity object being processedServices(IServiceProvider) — DI container for accessing services
Example Script Code (written in Web UI):
// Script code stored in MetaScript.Code (with ObjectName and EventName)
// Direct C# script, not a full class
// Access entity properties dynamically
if (Entity.Phone == null || !Entity.Phone.StartsWith("+"))
throw new Exception("Phone number must start with +");
// Access services via ScriptContext.Services
var logger = Context.Services.GetService<IZuloOneLogger>();
logger.LogInformation("Creating customer: {Name}", Entity.Name);
// Access data provider
var dataProvider = Context.Services.GetService<IDataProvider>();
var existingCustomer = dataProvider.Table("Customer")
.Query()
.Where(c => c.Email == Entity.Email)
.FirstOrDefault();
if (existingCustomer != null)
throw new Exception("Customer with this email already exists");
Supported Event Types (Lifecycle Events):
OnBeforeInsert— before inserting a new recordOnAfterInsert— after inserting a new recordOnBeforeUpdate— before updating a recordOnAfterUpdate— after updating a recordOnBeforeDelete— before deleting a recordOnAfterDelete— after deleting a recordOnValidateField— field-level validation (fieldName parameter available)OnBeforePost— before posting document (for documents)OnAfterPost— after posting document (for documents)
Runtime Execution Mechanism:
When system starts:
- Loads all metadata objects and their associated events from database
- Compiles scripts using
CSharpScript.Create()withScriptContextas globals type - Caches compiled scripts in memory dictionary (or Redis for multi-instance)
- On event trigger → executes corresponding compiled script
Implementation Example:
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
public class ScriptEngine
{
private readonly Dictionary<string, Script<object>> _compiledScripts = new();
public void Register(string objectName, string eventName, string code)
{
var key = $"{objectName}.{eventName}";
var script = CSharpScript.Create(
code,
globalsType: typeof(ScriptContext),
options: ScriptOptions.Default
.AddReferences(typeof(ScriptContext).Assembly)
.AddImports("System", "System.Linq", "ZuloOne.Core")
);
script.Compile();
_compiledScripts[key] = script;
}
public async Task ExecuteAsync(string objectName, string eventName, ScriptContext context)
{
var key = $"{objectName}.{eventName}";
if (_compiledScripts.TryGetValue(key, out var script))
await script.RunAsync(context);
}
}
public class ScriptContext
{
public dynamic Entity { get; set; } // The entity being processed
public IServiceProvider Services { get; set; } // DI container
public string FieldName { get; set; } // For OnValidateField events
}
Sandbox Security:
To prevent scripts from "breaking out":
- Strict globalsType — only
ScriptContextavailable - Limited assemblies — only allowed assemblies:
- System.dll (core types)
- System.Linq.dll
- ZuloOne.Core.dll (system APIs)
- Custom business assemblies (if configured)
- Prohibited namespaces — File, Network, Process, etc. are blocked
- Declarative checks — static analysis detects dangerous code patterns
Versioning and Audit Trail:
- Script version history stored in
MetaScriptHistorytable - Track who changed what and when
- Rollback capability to previous script version
- Compare script versions side-by-side
- Version tracking per script (not per event)
Metadata Versioning and Rollback:
- All metadata objects have version tracking
- Version history stored for metadata objects:
MetaDictionary,MetaDocumentType,MetaDocumentLine,MetaRegister,MetaDataEntity,MetaService, etc.- Field definitions (
MetaDictionaryProperty,MetaDocumentHeaderField,MetaDocumentLineField) - Status definitions (
MetaDocumentStatus)
- Rollback mechanism — revert metadata to previous working version in case of production bugs:
- Version snapshots — system automatically creates version snapshots before metadata changes
- Rollback UI — Web interface provides rollback functionality:
- View version history for any metadata object
- Compare versions side-by-side (what changed)
- Select previous working version
- Execute rollback (revert to selected version)
- Rollback API —
POST /api/metadata/{objectType}/{objectMetaId}/rollback/{version} - Automatic snapshots — created before:
- Metadata object modifications (fields, properties)
- Code changes (script compilation)
- Extension creation
- Package installation
- Production rollback workflow:
- Bug detected in production after release
- Identify problematic metadata object(s)
- View version history — find last known working version
- Compare versions — see what changed between working and broken version
- Execute rollback — revert to last working version
- System automatically:
- Restores metadata object to previous version
- Recompiles affected scripts if needed
- Updates runtime types
- Invalidates affected caches
- System returns to working state without full restore
- Selective rollback — can rollback:
- Single metadata object (one dictionary, one document type)
- Related objects (document type + its fields + statuses)
- All objects from a package (if package caused issues)
- Rollback safety:
- Rollback creates new version (doesn't delete versions)
- Can rollback rollback (undo if needed)
- Audit log tracks all rollback operations
- Rollback preserves data — only metadata changes, business data untouched
Debugging:
- All script execution logged via
IZuloOneLogger - Script errors captured with stack traces
- Can set breakpoints (future enhancement)
- Test scripts directly from Web UI before saving
🔧 Engine Components#
RuntimeCompiler#
Compiles C# business logic code using Roslyn at runtime. Supports two compilation modes:
- Full Assembly Compilation — for extensions and complex business logic (full classes)
- Script Compilation — for lightweight event handlers (CSharpScript API)
Script Compilation (CSharpScript):
- Uses
Microsoft.CodeAnalysis.CSharp.Scriptingfor lightweight script execution - Compiles event handler scripts into
Script<object>objects - Caches compiled scripts in memory/Redis
- Faster compilation and execution for simple event handlers
Full Assembly Compilation:
- Uses
Microsoft.CodeAnalysis(full Roslyn compiler) for complete classes - Generates strongly-typed classes from metadata
- Compiles into DLL assemblies for extensions and complex logic
Key features:
- Dynamic model generation — generates C# classes from metadata using Roslyn or Reflection.Emit
- Runtime compilation — compiles C# code (events, extensions, scripts) into in-memory or on-disk DLLs
- Recompilation on metadata changes — automatically regenerates and recompiles models when metadata changes
- Assembly generation — creates DLL assemblies for:
- Entity models (from MetaDictionary, MetaDocumentType, MetaRegister)
- Enum types (from MetaEnum, compiles to standard C# enum)
- Event handlers (from MetaScript where ScriptType="EventHandler")
- Service classes (from MetaScript where ScriptType="ServiceClass")
- Extension classes (from MetaScript where ScriptType="ExtensionClass")
- DataEntity processing code (from MetaScript where ScriptType="DataEntityScript")
- Type generation — generates strongly-typed classes with properties matching metadata fields
- Enum generation — generates standard C# enum types from MetaEnum (merges extension enum values into base enum)
- Extension inheritance — supports method overriding with
base()calls - Assembly caching — caches compiled assemblies for performance
- Hot-reload support — reloads assemblies without restarting system
Compilation process:
- Reads metadata definitions (including MetaEnum and MetaEnumValue)
- Generates C# source code for:
- Enum types (from MetaEnum → standard C# enum, merging extension values)
- Entity models (from MetaDictionary, MetaDocumentType, MetaRegister)
- Service classes (from MetaScript where ScriptType="ServiceClass")
- Event handlers (from MetaScript where ScriptType="EventHandler")
- Extension classes (from MetaScript where ScriptType="ExtensionClass")
- Compiles source via Roslyn into assembly (DLL)
- Loads assembly using
AssemblyLoadContext(plugin system) - Caches assembly for reuse
- On metadata change: invalidates cache, regenerates, recompiles
Enum Compilation Process:
- Reads
MetaEnumrecords (base enums and extensions) - For each enum:
- Reads
MetaEnumValuerecords (sorted by DisplayOrder) - If extension (
IsExtension=true), merges with base enum values - Generates C# enum code:
public enum EnumNoYes { No = 0, Yes = 1 }
- Reads
- Compiles enum types into assembly alongside entity models
- Field types using Enum EDT are typed as the generated enum (type-safe)
ObjectFactory#
Creates instances of runtime-generated entity classes. Provides dependency injection for entities and business logic code.
Features:
- Instantiates entity classes generated from metadata
- Dependency Injection — injects services into event handlers, extensions, and scripts:
IZuloOneLogger— loggingIDataProvider— data accessIEventBus— event publishingITransaction— transaction managementIHttpClient— HTTP client for integrations- Custom services registered in DI container
- Factory pattern for entity creation
EventDispatcher#
Manages event subscription and execution. Routes events to appropriate handlers based on event type and entity.
DataContextBuilder#
Builds Entity Framework DbContext from metadata. Generates DbSet properties for each metadata object (Reference, Document, Register). Handles ORM-level abstraction.
Features:
- Dynamic DbContext generation — creates
DbContextclass from metadata - DbSet generation — generates
DbSet<T>for each metadata object - ORM abstraction — system decides how models are mapped to SQL
- Migration generation — generates Entity Framework migrations automatically
- Schema synchronization — syncs database schema with metadata definitions
- Multi-database support — works with SQL Server, PostgreSQL, MySQL (via EF Core providers)
SchemaSynchronizer#
Synchronizes database schema with metadata definitions. Generates and applies migrations automatically.
Key features:
- Detects schema differences between metadata and database
- Generates SQL migrations (CREATE TABLE, ALTER TABLE, etc.)
- Handles EDT-based field generation with proper types, lengths, and precision
- Resolves EDT to database types (no separate EDTResolver needed):
- String EDT → VARCHAR/NVARCHAR based on length and IsUnicode
- Decimal EDT → DECIMAL/NUMERIC with precision
- Int EDT → INT/BIGINT based on length
- DateTime EDT → DATETIME/DATETIME2/TIMESTAMP (depending on provider and HasTime/HasTimezone)
- Date EDT → DATE
- Boolean EDT → BIT/BOOLEAN/TINYINT(1) (depending on database provider)
- Guid EDT → UNIQUEIDENTIFIER/UUID/BINARY(16) (depending on database provider)
- Binary/Blob EDT → VARBINARY(MAX)/BYTEA/BLOB (depending on provider and StorageType)
- If StorageType=Database: stores binary data in BLOB column
- If StorageType=FileSystem or CloudStorage: stores file path/URL in VARCHAR column
- Reference EDT → INT/BIGINT column for Id storage (based on referenced dictionary Id type)
- Creates reference columns from Reference EDT fields (no FK constraints)
- Supports multiple database providers (SQL Server, PostgreSQL, MySQL)
- Handles database provider differences directly in schema generation
Note: EDT resolution logic is handled directly in SchemaSynchronizer and DataContextBuilder when generating database schema. No separate EDTResolver.cs file needed — resolution logic is part of schema generation process.
What "this table already exists" means
Detection asks that question for every dictionary, table part, register, document and link table, and it must return the same answer the runtime would get. Every generated statement quotes its identifiers and leaves them unqualified, so "exists" means an identifier of exactly this spelling, resolvable through the current schema — not "a row with a similar name somewhere in the catalog".
Two members of ISqlDialect carry the engine differences:
| SQL Server | PostgreSQL | |
|---|---|---|
TableExistsQuery |
OBJECT_ID(N'[Name]', N'U') |
to_regclass('"Name"') |
CurrentSchemaExpression |
SCHEMA_NAME() |
current_schema() |
IdentifiersAreCaseSensitive |
false |
true |
Both the live probe and the per-pass catalog snapshot (LoadCatalogAsync) are
built on them: the snapshot filters INFORMATION_SCHEMA by
CurrentSchemaExpression and keys its dictionaries with the comparer
IdentifiersAreCaseSensitive implies.
They have to agree, because only the snapshot is ever consulted during a
detection pass. When they did not, on PostgreSQL a table left as deliveryterm
satisfied the case-insensitive snapshot, no CREATE was queued,
POST /api/schema/sync answered "Schema synchronized successfully" — it
returns success whenever SyncAsync does not throw, and doing nothing does not
throw — and every read of the table raised
42P01 relation "DeliveryTerm" does not exist. SQL Server hid it entirely: the
engine is case-insensitive, so the two paths agreed there. Pair a sync with
GET /api/schema/detect when you need evidence a table was actually created.
🛠️ Services#
DataService#
Provides high-level CRUD operations on entities. Handles validation, events, and transaction management.
MetadataService#
Manages metadata CRUD operations. Validates metadata consistency and relationships.
EventService#
Manages event handlers registration and execution. Provides event debugging capabilities.
Responsibilities:
- Loads event scripts from
MetaScripttable (where ScriptType="EventScript") - Resolves script code from
MetaScripttable viaScriptIdreference - Compiles scripts using
CSharpScript.Create()via ScriptEngine - Caches compiled scripts for performance
- Executes scripts on event triggers
- Provides ScriptContext with Entity and Services
- Logs script execution for debugging
- Handles script errors and exceptions
Script Execution Flow:
- Event triggered (e.g., OnBeforeInsert for Customer)
- EventService queries
MetaScripttable:SELECT * FROM MetaScript WHERE ObjectName = 'Customer' AND EventName = 'OnBeforeInsert' AND ScriptType = 'EventScript' AND IsActive = true - Gets compiled script from cache (or compiles if not cached)
- Creates ScriptContext with Entity and Services
- Executes compiled script with context
- Handles exceptions (validation errors, etc.)
- Logs execution result
ExtensionService#
Manages business logic extension classes registration and execution (stored in MetaScript).
Responsibilities:
- Loads extension definitions from
MetaScripttable whereScriptType="ExtensionClass" - Uses
BaseClassNamefield to identify base class being extended - Resolves extension class code from
MetaScript.Codefield - Compiles extension classes into assemblies via Roslyn
- Resolves extension chain (multiple extensions for same base class via
BaseClassName) - Executes extensions with proper inheritance chain
- Handles
base()method calls to parent classes - Manages extension priority and execution order
- Supports hot-reload of extensions
RegisterEngineService#
Manages register engine lifecycle and execution.
Responsibilities:
- Creates register engines based on
MetaRegister.RegisterEngineType- "Standard" → creates
StandardRegisterEngine - "FIFO" → creates
FIFORegisterEngine - "Financial" → creates
FinancialRegisterEngine - "FlatHistory" → creates
FlatHistoryRegisterEngine - "Custom" → compiles and creates
CustomRegisterEnginefromMetaScript(viaEngineScriptId)
- "Standard" → creates
- Initializes engines with register configuration (headers, lines, dimensions)
- Handles document posting to registers via engines
- Processes register queries (GetBalance, GetTurnover, GetRemains)
- Manages custom engine compilation from
MetaScript(whenRegisterEngineType = "Custom") - Caches engine instances for performance
- Supports hot-reload of custom engines when scripts change
Engine Factory Pattern:
// RegisterEngineFactory creates appropriate engine
var engine = RegisterEngineFactory.Create(
registerEngineType: "FIFO", // or "Standard", "Financial", "FlatHistory", "Custom"
customScriptId: null // or ScriptId if Custom
);
// Initialize with register configuration
engine.Initialize(register, headers, lines);
// Use engine for posting and queries
engine.Post(document, movements);
var balance = engine.GetBalance(date, dimensions);
Register Posting Flow:
- Document posts → generates register movements
- RegisterEngineService resolves register's engine type from
MetaRegister - Gets or creates engine instance (from cache or factory)
- Calls
engine.Post(document, movements) - Engine processes movements according to its logic:
- Standard → accumulates values (sum, count)
- FIFO → maintains queue, consumes from oldest receipts first
- Financial → accumulates Debit/Credit, collapses if balance = 0
- FlatHistory → just records history without accumulation
- Custom → executes custom script logic (compiled from MetaScript)
Custom Engine Example:
// In MetaScript (ScriptType = "RegisterEngineScript")
public class CustomWarehouseEngine : IRegisterEngine
{
public void Post(Document document, RegisterMovement[] movements)
{
// Custom posting logic
// e.g., special cost calculation, custom balance rules, etc.
}
public RegisterBalance GetBalance(DateTime date, RegisterDimension[] dimensions)
{
// Custom balance calculation
}
}
MigrationService#
Handles database schema migrations. Detects changes in metadata and generates appropriate SQL migrations.
Auto-Migration on Startup:
- Automatically applies migrations when Core starts
- Checks database schema against metadata definitions
- Generates and applies missing migrations automatically
- No manual
dotnet ef database updaterequired - Ensures database is always in sync with metadata
Migration Flow on Startup:
- Core starts → MigrationService initializes
- Reads all metadata from database
- Compares with current database schema
- Generates migration scripts for differences
- Applies migrations automatically
- Core continues startup after migrations complete
Manual Migration (Optional):
- Can disable auto-migration via configuration:
"AutoMigrate": false - Manual migration:
dotnet ef database update(for advanced users)
ProjectService#
Manages development projects — isolated workspaces for developers.
Responsibilities:
- Creates and manages
MetaProjectrecords - Manages
MetaProjectObjecttable (many-to-many relationship) - Assigns objects to projects (creates MetaProjectObject entries)
- Provides project isolation (developers see only objects in their project via MetaProjectObject)
- Handles project export/import
- Manages project versions and merging
- Coordinates multi-developer collaboration
- Allows same object to be in multiple projects
Project Isolation:
- Each developer works in their own project
- Objects with
ProjectIdare only visible to project members - Projects can share base objects (from system models)
- Projects can reference objects from other projects (with permissions)
Project Export/Import:
- Export project to ZIP file (metadata + code + tests)
- Import project from ZIP (dev → production)
- Validates project structure and dependencies
- Handles conflicts with existing objects
Project Versioning:
- Projects have versions
- Track changes per project
- Merge capabilities between projects
TestService#
Manages unit and integration tests for developer code.
Responsibilities:
- Manages test scripts stored in
MetaScripttable (whereScriptType="UnitTest"orScriptType="IntegrationTest") - Tests are regular scripts in
MetaScripttable (not separate table):ScriptType="UnitTest"orScriptType="IntegrationTest"ObjectType="Test"ObjectName— test nameObjectMetaId— references metadata object being tested (Dictionary, Document, Register, etc.)Code— full C# test code (uses test framework: NUnit, xUnit, etc.)
- Executes test suites
- Reports test results (pass/fail, coverage)
- Integrates with Developer Studio for test execution
Test Types:
- UnitTest — tests individual functions/scripts
- IntegrationTest — tests object interactions (e.g., document posting with register updates)
Test Execution:
- Run tests for specific project
- Run tests for specific object
- Run all tests in project
- Continuous testing (auto-run on code changes)
Test Reporting:
- Test results (pass/fail count)
- Execution time
- Coverage metrics
- Failure details with stack traces
PackageService#
Manages package installation, updates, and distribution.
Responsibilities:
- Imports ZIP packages with metadata and code
- Validates package structure and dependencies
- Installs metadata to database
- Compiles and loads code (Extensions, Events)
- Applies database migrations from package
- Tracks package versions
- Handles package updates and rollbacks
- Manages package conflicts and resolution
Package Installation Flow:
- Upload ZIP package via API/UI
- Validate package structure and manifest
- Check dependencies and conflicts
- Create installation backup
- Import metadata (EDT, Dictionaries, Documents, etc.)
- Compile and load code (Extensions, Events, DataEntities)
- Apply database migrations
- Verify installation
- Record package version
🌐 API Controllers#
DataController#
RESTful API for data operations:
GET /api/data/{tableName}— query recordsPOST /api/data/{tableName}— create recordPUT /api/data/{tableName}/{id}— update recordDELETE /api/data/{tableName}/{id}— delete record
MetadataController#
RESTful API for metadata management:
GET /api/metadata/tables— list all tablesPOST /api/metadata/tables— create table definitionPUT /api/metadata/tables/{name}— update table definitionGET /api/metadata/tables/{name}/fields— get table fields
EventController#
API for event management:
POST /api/events/{tableName}/test— test event handlerGET /api/events/{tableName}— list event handlers
ODataController#
Public OData 4 host (DataEntityHostController) for designed data entities:
/odata— service document of active collections/odata/$metadata— EDMX/odata/{PublicCollectionName}—$filter,$select,$orderby,$top,$skip,$count- POST/PATCH/DELETE on the collection/key write the primary source table (405 if
IsReadOnly) - Directory JWT (
aud=ZuloOne:PublicUrl) or SPA token; object permissions + source-table RLS - Scripts use the same mapping via
IDataEntityManager(GetService); entity ACL does not apply in-process
Example:
GET /odata/CustomersV2?$filter=IsActive eq true&$select=ID,Name
Authorization: Bearer …
ProjectController#
API for project management:
GET /api/projects— list all projects (filtered by user/status)POST /api/projects— create new projectGET /api/projects/{id}— get project detailsPUT /api/projects/{id}— update projectDELETE /api/projects/{id}— delete project (only if not in production)POST /api/projects/{id}/export— export project to ZIP (includes all objects + tests)POST /api/projects/import— import project from ZIPPOST /api/projects/{id}/promote— promote project to production (removes MetaProjectObject entries)POST /api/projects/{id}/merge— merge with another projectGET /api/projects/{id}/objects— list all objects in project (via MetaProjectObject)POST /api/projects/{id}/objects— add object to project (create MetaProjectObject entry)DELETE /api/projects/{id}/objects/{objectType}/{objectId}— remove object from project
TestController#
API for test execution:
GET /api/tests— list all tests (filtered by project)POST /api/tests— create new testGET /api/tests/{id}— get test detailsPUT /api/tests/{id}— update testDELETE /api/tests/{id}— delete testPOST /api/tests/{id}/run— run specific testPOST /api/tests/run-project/{projectId}— run all tests in projectPOST /api/tests/run-object/{objectId}— run tests for specific objectGET /api/tests/{id}/results— get test execution resultsGET /api/tests/coverage/{projectId}— get test coverage for project
PackageController#
API for package management:
POST /api/packages/upload— upload ZIP packagePOST /api/packages/{id}/install— install packageGET /api/packages— list installed packagesGET /api/packages/{id}— get package detailsPOST /api/packages/{id}/rollback— rollback to previous versionPOST /api/packages/export— export current configuration as packagePOST /api/packages/import-project— import project as package (promotes to production)
MetadataController (Extended for Runtime Access)#
RESTful API for metadata and runtime model access:
GET /api/metadata/tables— list all tablesGET /api/metadata/tables/{name}— get table definitionGET /api/metadata/tables/{name}/fields— get table fieldsGET /api/metadata/runtime/types— get compiled runtime type informationGET /api/metadata/package— get current metadata package (JSON + DLL)POST /api/metadata/reload— trigger metadata reload (notifies subscribers)
gRPC Service (alternative to REST):
MetadataService.GetTables()— get all table definitionsMetadataService.GetTable(string name)— get specific tableRuntimeService.GetTypeInfo(string typeName)— get runtime type informationMetadataService.Subscribe(MetadataChanged)— subscribe to metadata change events
🧱 Extensibility and Extensions#
Business Logic Extensions (stored in MetaScript)#
Business logic extensions extend C# classes (like InvoicePosting):
- Extensions can only be created in client models (not in system models)
- Extensions reference base classes from system models or other client models
- Extensions are isolated within their model namespace
Example:
Core Model:
- InvoicePosting class (base class)
Custom Model:
- InvoicePostingExt (extends Core.InvoicePosting)
- Can override Post() method
- Can call base.Post() to invoke Core logic
- Cannot modify Core.InvoicePosting directly
Metadata Object Extensions#
Metadata object extensions extend metadata objects (Dictionaries, Documents, Registers, DataEntities):
- Can create extensions for existing dictionaries, documents, registers, DataEntities
- Extension creates new metadata object record (same table type) with
IsExtension=trueandBase*Idpointing to base - Supports extension chains across layers (Base → ISV → Customer)
- Fields edited directly in object editor (not separate page)
- Scripts for extensions are stored in
MetaScriptand linked to extension object (same as base objects)
Extension Chain Example:
Layer: System (CRM Model)
- Dictionary "CustomerTable" (LayerId=System, IsExtension=false, BaseDictionaryId=null)
- Fields: Id, Code, Name, Phone
Layer: ISV (ISV "Roga" Model)
- Dictionary "CustomerTableExt" (LayerId=ISV, IsExtension=true, BaseDictionaryId="CustomerTable")
- Additional Fields: Email, Website (editable in extension, via MetaDictionaryProperty)
- Inherited Fields: Id, Code, Name, Phone (read-only, shown from base)
- Scripts: linked via MetaScript with ObjectName="CustomerTableExt"
Layer: Customer (Customer "Contoso" Model)
- Dictionary "CustomerTableExt2" (LayerId=Customer, IsExtension=true, BaseDictionaryId="CustomerTableExt")
- Additional Fields: TaxId, CustomField1 (editable in extension)
- Inherited Fields:
* From Base: Id, Code, Name, Phone (read-only)
* From ISV: Email, Website (read-only)
- Scripts: linked via MetaScript with ObjectName="CustomerTableExt2"
Visual Editor Behavior:
- When editing extension, shows:
- Inherited Fields Section (read-only, grayed out):
- All fields from base object and all extensions in chain (resolved via Base*Id chain)
- Shows which layer each field comes from
- Extension Fields Section (editable):
- Fields added in this extension (stored in MetaDictionaryProperty/MetaDocumentHeaderField/etc. for this extension)
- Can add new fields here
- Inherited Fields Section (read-only, grayed out):
🏗️ System Layers#
MetaLayer defines system layers for extension hierarchy and compilation order.
Layer Concept#
Why Layers:
- Compilation Order — extensions must compile in correct order (Base → ISV → Customer)
- Extension Chains — supports multi-level extensions (Base → ISV Extension → Customer Extension)
- Isolation — each layer is isolated (ISV can't see Customer, Customer can see ISV)
- Deployment — layers can be deployed independently
Layer Types#
Built-in Layers:
-
System Layer (Order=0)
- Core system objects
- Base dictionaries, documents, registers
- Cannot be modified by ISV or Customer
- Example: Dictionary "CustomerTable" in CRM model
-
ISV Layer (Order=1)
- ISV (Independent Software Vendor) extensions
- ISV creates extensions on System objects
- ISV can't modify System, can add extensions
- Example: ISV "Roga" creates "CustomerTableExt"
-
Customer Layer (Order=2)
- Customer-specific extensions
- Customer creates extensions on ISV extensions or System objects
- Customer can't modify System/ISV, can add extensions
- Example: Customer "Contoso" creates "CustomerTableExt2"
Layer Structure#
MetaLayer Table:
MetaLayer:
Id | Name | LayerType | Order
1 | System | System | 0
2 | ISV | ISV | 1
3 | Customer | Customer | 2
Layer Assignment:
- Models have
LayerIdfield — determines which layer the model belongs to - Objects inherit layer from their model —
LayerIdinherited fromMetaModel.LayerId - System models:
LayerId = 1(System) → all objects in System models inherit LayerId=1 - ISV models:
LayerId = 2(ISV) → all objects in ISV models inherit LayerId=2 - Customer models:
LayerId = 3(Customer) → all objects in Customer models inherit LayerId=3 - Objects can also have explicit
LayerIdfor overrides (optional), but typically inherit from model
Extension Chain Compilation#
Compilation Order:
- System Layer (Order=0) — compile all base objects
- ISV Layer (Order=1) — compile ISV extensions (reference System)
- Customer Layer (Order=2) — compile Customer extensions (reference ISV + System)
Example Chain:
System: CustomerTable
→ Compiles to: CustomerTable class
→ Fields: Id, Code, Name, Phone
ISV Roga: CustomerTableExt (extends CustomerTable)
→ Compiles to: CustomerTableExt : CustomerTable
→ Inherits: Id, Code, Name, Phone
→ Adds: Email, Website
Customer Contoso: CustomerTableExt2 (extends CustomerTableExt)
→ Compiles to: CustomerTableExt2 : CustomerTableExt
→ Inherits: Id, Code, Name, Phone, Email, Website
→ Adds: TaxId, CustomField1
Runtime Type:
- Final compiled type:
CustomerTableExt2 - Contains all fields from entire chain
- Can access all layers' fields in code
The system supports extending base functionality through extensions with inheritance and method overriding capabilities.
Business Logic Extension Pattern#
Extensions allow extending business logic classes (like document posting, validation, etc.) without modifying core code.
Basic Example:
// Base class (core system)
public class InvoicePosting {
public virtual void Post() {
// Core posting logic
UpdateAccountBalance();
CreateJournalEntries();
}
}
// Extension class (extensible by user)
[ExtensionOf(typeof(InvoicePosting))]
public class InvoicePostingExt : InvoicePosting {
public override void Post() {
base.Post(); // Call parent class logic first
SendInvoiceToCRM(); // Then execute extension logic
}
}
Key Features:
- Extension class inherits from base class
- Can override virtual methods
- Can call base class methods using
base.MethodName() - Extensions execute after base logic (or in custom order)
- Compiled separately from core code
- Supports hot-reload of extensions without restarting system
Metadata Object Extension Pattern#
Extensions allow extending metadata objects (Dictionaries, Documents, Registers, DataEntities) by creating new object records that inherit from base.
Extension Creation:
- Developer selects base object (e.g., Dictionary "CustomerTable")
- Clicks "Create Extension" button or dropdown action
- System creates new object record in same table (e.g., new
MetaDictionaryrecord "CustomerTableExt") - System sets
IsExtension=trueandBaseDictionaryId="CustomerTable"(orBaseDocumentTypeId,BaseRegisterId,BaseDataEntityIdfor other types) - Extension inherits all fields from base (shown read-only in editor, resolved via Base*Id chain)
Extension Chain Resolution:
- System follows
Base*Idchain: Extension → Base → Base.Base → ... - For Dictionary: follows
BaseDictionaryIdchain - For Document: follows
BaseDocumentTypeIdchain - For Register: follows
BaseRegisterIdchain - For DataEntity: follows
BaseDataEntityIdchain
Scripts and Events:
- Scripts stored in
MetaScriptand linked to extension object (viaObjectNamefield) - Extension can override events from base (same event names, different scripts)
- Extension can add new events
Extension Chain:
- Supports multi-level extensions: Base → ISV Extension → Customer Extension
- Each extension adds new fields while inheriting from chain
- Compilation respects layer order (System → ISV → Customer)
Visual Editor:
- When editing extension, shows:
- Inherited Fields (read-only, grayed out) — from all base objects in chain (resolved via Base*Id chain)
- Extension Fields (editable) — fields added in this extension (stored in field tables for this extension)
- Can see entire extension chain and field sources
Compilation:
- Extensions compile to classes that inherit from base
- Final type includes all fields from entire chain
- Runtime object has all fields accessible
Business Logic Extension Storage#
Business logic extensions stored in MetaScript table with ScriptType="ExtensionClass":
Id— script identifierName— extension class name (e.g., "InvoicePostingExt")ScriptType = "ExtensionClass"— identifies as extension classObjectName— extension class name (same as Name)BaseClassName— base class being extended (e.g., "InvoicePosting")Code— full C# code of extension classModelId— model this extension belongs toIsActive— enable/disable extension (can be added as field if needed)Priority— execution order when multiple extensions exist (can be added as field if needed)- No separate MetaExtension table — all extension metadata and code in MetaScript
Extension Execution Flow#
When InvoicePosting.Post() is called:
- System checks for extensions of
InvoicePosting - If extension exists:
- Loads extension assembly
- Creates instance of extension class
- Calls extension's
Post()method - Extension calls
base.Post()which executes base logic - Extension executes its additional logic
- If no extension:
- Executes base class method directly
Example with Multiple Extensions:
// Base
public class InvoicePosting {
public virtual void Post() { /* core logic */ }
}
// Extension 1 (Priority: 1)
[ExtensionOf(typeof(InvoicePosting))]
public class InvoicePostingExt1 : InvoicePosting {
public override void Post() {
base.Post();
SendToAccountingSystem(); // Additional logic 1
}
}
// Extension 2 (Priority: 2, extends Ext1)
[ExtensionOf(typeof(InvoicePostingExt1))]
public class InvoicePostingExt2 : InvoicePostingExt1 {
public override void Post() {
base.Post(); // Calls Ext1.Post(), which calls base.Post()
SendToCRM(); // Additional logic 2
}
}
// Execution: Ext2.Post() → Ext1.Post() → Base.Post()
Use Cases#
- Custom business logic — add company-specific processing
- Integration — send data to external systems (CRM, accounting)
- Validation — additional validation rules
- Notifications — send notifications on document posting
- Logging and auditing — custom audit trails
- Multi-tenant customization — different extensions per tenant
Extension Registration#
Extensions are registered in metadata:
- Extension stored in
MetaScripttable withScriptType="ExtensionClass" MetaScript.BaseClassNameidentifies which base class is extendedMetaScript.Codecontains full extension class code- All extension metadata and code in single
MetaScriptrecord - Created via Web UI code editor
- Compiled on save
- Automatically loaded on system startup
- Can be enabled/disabled without recompilation
📁 Development Projects#
MetaProject represents a development workspace — an isolated environment where developers create and test their code before deploying to production.
Project Concept#
Why Projects:
- Developer Isolation — multiple developers can work simultaneously without conflicts
- Development → Production — develop in dev project, export, import to production
- Testing — each project has its own test suite
- Version Control — track changes per project
- Collaboration — teams can work on different projects, merge when ready
Project Structure#
MetaProject Table:
MetaProject:
Id | Name | Description | Version | Author | Created | Modified | Status
1 | InvoiceModule | Invoice processing module | 1.0.0 | dev1 | 2025-01-01 | 2025-01-15 | Development
2 | TaxModule | Tax calculations | 1.0.0 | dev2 | 2025-01-02 | 2025-01-10 | Development
3 | ProdConfig | Production configuration | 2.5.0 | admin | 2025-01-01 | 2025-01-20 | Production
Project Status:
- Development — active development, not yet in production
- Testing — ready for testing
- Production — deployed to production
- Archived — no longer active
Project Association#
Objects belong to Models, Projects reference objects (many-to-many):
- Objects belong to Models (ModelId) — permanent association
- Projects reference objects via
MetaProjectObjecttable (many-to-many relationship) - One object can be in multiple projects simultaneously
- Objects without project references — system/production objects (can be referenced by projects later)
- Projects can reference system objects but cannot modify them
Example:
System Objects (belong to Models):
- Core Model: Dictionary "Customers" (ModelId=1)
- Core Model: Document "Invoice" (ModelId=1)
- Custom Model: Register "TaxRegister" (ModelId=5)
Project "InvoiceModule" (ProjectId=1):
MetaProjectObject entries (which objects are in this project):
- ProjectId=1, ObjectType="Dictionary", ObjectId="InvoiceTypes" (references MetaDictionary.Id)
- ProjectId=1, ObjectType="Document", ObjectId="Invoice" (references MetaDocumentType.Id)
- ProjectId=1, ObjectType="Script", ObjectId="InvoicePostingExt" (references MetaScript.Id, ScriptType="ExtensionClass")
- ProjectId=1, ObjectType="Script", ObjectId="InvoiceOnBeforePost" (references MetaScript.Id, ScriptType="EventScript")
Project "TaxModule" (ProjectId=2):
MetaProjectObject entries:
- ProjectId=2, ObjectType="Register", ObjectId="TaxRegister" (references MetaRegister.Id)
- ProjectId=2, ObjectType="Script", ObjectId="TaxCalculationExt" (references MetaScript.Id, ScriptType="ExtensionClass")
Same object in multiple projects (many-to-many):
- Document "Invoice" (ModelId=1) can be in both "InvoiceModule" and "CommonDocuments" projects
- MetaProjectObject entries:
* ProjectId=1, ObjectType="Document", ObjectId="Invoice"
* ProjectId=3, ObjectType="Document", ObjectId="Invoice" (same MetaDocumentType.Id referenced by 2 projects)
Project Export/Import#
Export Project:
- Select project to export
- System generates ZIP package with:
- Project metadata (MetaProject record)
- All objects in project (Dictionaries, Documents, Registers, Scripts, Tests)
- Dependencies (referenced objects from system or other projects)
- Project version information
Export Format:
InvoiceModule-v1.0.0.zip
├── manifest.json
│ └── ProjectId: 1, Name: "InvoiceModule", Version: "1.0.0"
├── project.json
│ └── MetaProjectObject.json (list of objects in project)
├── metadata/
│ ├── MetaDictionary.json (objects referenced by project)
│ ├── MetaDocumentType.json
│ ├── MetaRegister.json
│ └── MetaScript.json
├── tests/
│ └── MetaTest.json (all tests in project)
└── dependencies.json (referenced system objects)
Import Project:
- Upload project ZIP file
- System validates project structure
- Checks for conflicts with existing objects
- Imports project objects (creates or updates)
- Resolves dependencies
- Can import to different environment (dev → production)
Import Options:
- Import to New Project — creates new project (for testing)
- Creates MetaProject record
- Creates MetaProjectObject entries for all objects in package
- Import to Existing Project — updates existing project (merge)
- Adds new MetaProjectObject entries
- Updates existing objects if needed
- Import to Production — promotes project to production
- Objects remain in their Models
- Removes MetaProjectObject entries (objects become system-wide)
- Objects are no longer isolated to project
Project Testing#
Test Structure:
Tests are stored in MetaScript table (not separate table):
MetaScript (for tests):
MetaId | Name | ScriptType | ObjectType | ObjectName | ObjectMetaId | Code
GUID_1 | TestInvoicePosting | UnitTest | Test | TestInvoicePosting | GUID_INVOICE | [Test] public void TestInvoicePosting() { ... }
GUID_2 | TestInvoiceValidation | UnitTest | Test | TestInvoiceValidation | GUID_INVOICE | [Test] public void TestInvoiceValidation() { ... }
GUID_3 | TestInvoiceWithRegister | IntegrationTest | Test | TestInvoiceWithRegister | GUID_INVOICE | [Test] public void TestInvoiceWithRegister() { ... }
Test Script Example (stored in MetaScript):
// ScriptType = "UnitTest"
// Test for Invoice.OnBeforePost event
// Arrange
var invoice = new Invoice
{
Number = "INV-001",
CustomerId = 1,
TotalAmount = 1000
};
// Act
invoice.Post();
// Assert
Assert.IsTrue(invoice.Status == "Posted");
Assert.IsNotNull(invoice.PostedDate);
Test Execution:
- Run all tests in project
- Run tests for specific object
- Run specific test by name
- Continuous testing (auto-run on save)
Test Results:
- Pass/Fail status
- Execution time
- Failure messages and stack traces
- Coverage metrics (which code paths were tested)
Developer Workflow with Projects#
1. Create Project:
Developer → Developer Studio → New Project
- Name: "InvoiceModule"
- Description: "Invoice processing functionality"
- Version: "1.0.0"
2. Develop in Project:
Developer → Select Project "InvoiceModule"
→ Create Dictionary "InvoiceTypes" (belongs to Model, e.g., Custom Model)
→ System automatically adds to project: MetaProjectObject(ProjectId=1, ObjectType="Dictionary", ObjectId="InvoiceTypes")
→ Create Document "Invoice"
→ System adds: MetaProjectObject(ProjectId=1, ObjectType="Document", ObjectId="Invoice")
→ Write Event Script "Invoice.OnBeforePost"
→ System adds: MetaProjectObject(ProjectId=1, ObjectType="Script", ObjectId="InvoiceOnBeforePost")
→ Write Extension "InvoicePostingExt"
→ System adds: MetaProjectObject(ProjectId=1, ObjectType="Extension", ObjectId="InvoicePostingExt")
3. Write Tests:
Developer → Tests Tab → New Test
- Name: "TestInvoicePosting"
- Type: UnitTest
- Target: Invoice
- Write test script
→ Test saved with ProjectId=1
4. Run Tests:
Developer → Tests Tab → Run All Tests
→ Tests execute
→ See results: 5 passed, 2 failed
→ Fix failing tests
5. Export Project:
Developer → Project → Export
→ Generates InvoiceModule-v1.0.0.zip
→ Contains all objects + tests
6. Import to Production:
Admin → Package Management → Import Project
→ Upload InvoiceModule-v1.0.0.zip
→ System imports objects (they belong to their Models)
→ System removes MetaProjectObject entries (promotes to production)
→ Objects are now available system-wide (no longer project-isolated)
Project Collaboration#
Multi-Developer Workflow:
- Each developer has their own project
- Projects can be shared (multiple developers in one project)
- Projects can reference objects from other projects (read-only)
- Projects can be merged when ready
Merge Projects:
- Combine multiple projects into one
- Resolve conflicts (naming, dependencies)
- Validate merged project
- Export merged project
📦 Package Management (Model-Based)#
Package Format (ZIP)#
Packages are distributed as ZIP archives containing complete architecture configuration.
Package Structure:
PackageName-v1.0.0.zip
├── manifest.json // Package metadata (name, version, dependencies)
├── metadata/
│ ├── edt.json // Extended Data Types
│ ├── dictionaries.json // Dictionaries
│ ├── documents.json // Document types
│ ├── document-headers.json // Document header fields
│ ├── document-lines.json // Document line fields
│ ├── registers.json // Registers
│ ├── data-entities.json // DataEntities
│ ├── extensions.json // Extensions
│ └── events.json // Events
├── code/
│ ├── extensions/ // Extension C# code files
│ ├── events/ // Event handler C# code files
│ └── data-entities/ // DataEntity processing C# code
├── transformations.json // Data transformations (optional)
└── version.json // Version information
Note: No SQL files — database schema updated automatically via ORM from metadata. No pre-compiled DLLs — code recompiled on target system via Roslyn.
Manifest.json:
{
"name": "ERP-Base-Configuration",
"version": "1.0.0",
"description": "Base ERP configuration package",
"author": "ZuloOne Team",
"dependencies": [],
"targetVersion": ">=1.0.0",
"installSteps": [
"Import metadata",
"Compile code",
"Apply migrations",
"Verify installation"
]
}
Package Installation Process#
Step-by-step installation:
-
Upload Package
- User uploads ZIP file through Web UI
- System validates ZIP structure
- Extracts and validates manifest.json
-
Validation
- Check package version compatibility
- Verify dependencies (required packages)
- Detect conflicts with existing metadata
- Validate code syntax
-
Backup
- Create snapshot of current state
- Backup metadata to allow rollback
- Record current package versions
-
Installation
- Import metadata (EDT, Dictionaries, Documents, etc.)
- Compile C# code (Extensions, Events, DataEntities)
- Apply database migrations (if any)
- Load compiled assemblies
-
Verification
- Verify all metadata imported correctly
- Test code compilation
- Validate database schema
- Check for errors
-
Completion
- Record installed package version
- Update package registry
- Notify user of success/failures
Package Export#
Export current configuration as package:
- Export all metadata (EDT, Dictionaries, Documents, etc.)
- Export all code (Extensions, Events, DataEntities)
- Generate database migration scripts (current schema)
- Create ZIP package with manifest
- Download package for distribution
Package Versioning#
- Semantic versioning:
MAJOR.MINOR.PATCH(e.g., 1.2.3) - Version tracking: System tracks installed package versions
- Update support: Upgrade from one version to another
- Rollback: Revert to previous version if needed
- Rollback metadata objects to last working version
- Automatic version snapshots before changes
- Web UI for version comparison and rollback
- API endpoint for programmatic rollback
- Preserves business data — only metadata changes
Multi-Tenant Packages#
- Each tenant can have different packages installed
- Package isolation per tenant
- Tenant-specific customizations via packages
- Shared packages across tenants (optional)
🔄 Database Generation and Migrations#
Automatic Schema Synchronization#
- Detects differences between current database schema and metadata definitions
- Generates SQL migration scripts automatically based on EDT parameters
- Applies migrations via AutoSync feature
- Supports multiple database providers
Field Generation from EDT#
When generating database schema, the system uses EDT definitions:
Base EDT → Database Column:
- Int EDT (Length=32) →
INT - Int EDT (Length=64) →
BIGINT - Decimal EDT (Length=18, Precision=2) →
DECIMAL(18, 2) - String EDT (Length=100, IsUnicode=true) →
NVARCHAR(100)orVARCHAR(100)
Reference EDT → Reference Field:
- Reference EDT (ReferenceTableName="Addresses") →
- Column:
AddressId INT(stores reference Id, no FK constraint in database) - Relationship logic handled by system, not database constraints
- Column:
Automatic behavior:
- When Reference EDT is used in a field, system automatically:
- Creates reference column (
{FieldName}Id) storing the referenced table's Id - No Foreign Key constraint is created in database
- Relationship integrity is maintained by application logic
- Creates index on reference column for performance (optional, configurable)
- Creates reference column (
Note: All data access is through system API/OData interface. Database does not create Foreign Key constraints. Referential integrity on delete is enforced by RecordReferenceGuard (IDataService.DeleteAsync): incoming Reference EDT / table-part / link-table / register-dimension / ParentId / dictionary-list hits refuse the delete with 409 Conflict.
Migration Types#
-
Create Table — new tables from
MetaDictionary,MetaDocumentType,MetaRegister- Fields generated from EDT definitions with proper types
- Reference fields store Id values (no Foreign Key constraints)
-
Alter Table — add/modify/remove columns
- Column type changes based on EDT parameter changes
- Length/precision updates when EDT parameters change
-
Create Index — indexes for performance
- Indexes on reference columns (for query performance)
- Custom indexes from field properties
-
No Foreign Keys — relationships are handled by application logic, not database constraints
- All data access through system API/OData interface
-
Drop Table — remove obsolete tables
Document Structure Generation#
For documents (header + lines):
- Automatic class generation: When document type is created, system automatically generates:
- C# class for document type (e.g., "SalesOrder") with properties:
- DocumentType (MetaId reference)
- Status (from MetaDocumentStatus)
- Number, Date (standard document fields)
- All header fields (generated from MetaDocumentHeaderField)
- Default line type (
MetaDocumentLine) with name "{DocumentTypeName}Line" (e.g., "SalesOrderLine") - C# class for default line type with properties:
- DocumentType (MetaId reference to document type)
- LineNumber (int)
- DocumentId (reference to document header)
- All line fields (generated from MetaDocumentLineField)
- C# class for document type (e.g., "SalesOrder") with properties:
- Reads
MetaDocumentTypeto get document type definition - Reads
MetaDocumentHeaderFieldrecords (filtered byDocumentTypeMetaId) to generate header table fields - Reads
MetaDocumentLinerecords (filtered byDocumentTypeMetaId) to get all line types for document - For each line type:
- Reads
MetaDocumentLineFieldrecords (filtered byDocumentLineMetaId) to generate line table fields - Creates runtime table:
{LineTypeName}(e.g., "SalesOrderLines" for default line type "SalesOrderLine", "InvoiceDiscountLines" for additional line type)
- Reads
- Creates header table:
{DocumentTypeName}(e.g., "SalesOrder") - Links lines to header via
DocumentIdcolumn (stores header Id, no FK constraint) - Line number field generated automatically for each line type
- Each field uses its EDT to determine database column type
- Relationship between header and lines handled by application logic
- Optional script reference via
MetaDocumentType.ScriptIdfor document-specific logic
Register Structure Generation#
For registers (header + lines):
- Reads
MetaRegisterto get register type definition - Reads
MetaRegisterHeaderrecords (filtered byRegisterId) to generate header table fields - Reads
MetaRegisterFieldrecords (filtered byRegisterIdandFieldType="Line") to generate lines table fields - Creates two runtime tables:
{RegisterName}(header) and{RegisterName}Lines(lines) - Links lines to header via
RegisterIdcolumn (stores header Id, no FK constraint) - Each field uses its EDT to determine database column type
- Relationship between header and lines handled by application logic
- Optional script reference via
MetaRegister.ScriptIdfor register-specific logic
🔐 Security and Access Control#
User Management#
MetaUser represents system users — accounts that can authenticate and access the system.
Properties:
- Authentication — username/password authentication (can be extended with OAuth, LDAP, etc.)
- User profile — FirstName, LastName, Email, etc.
- Active status —
IsActiveflag to enable/disable accounts - Last login tracking —
LastLogintimestamp - Password security — passwords stored as hashes (never plain text)
User Table Structure:
MetaUser:
MetaId (Guid, PK) | UserName | Email | PasswordHash | IsActive | FirstName | LastName | LastLogin | Created | Modified
GUID_USER_1 | admin | [email protected] | $2a$10$... | true | Admin | System | 2025-01-15 | 2025-01-01 | 2025-01-15
GUID_USER_2 | jsmith | [email protected] | $2a$10$... | true | John | Smith | 2025-01-14 | 2025-01-02 | 2025-01-14
Role-Based Access Control (RBAC)#
MetaRole represents security roles — groups of permissions assigned to users.
Properties:
- System roles — built-in roles (Administrator, Developer, User, Guest)
- Custom roles — client-created roles
- Role assignment — users assigned to roles via
MetaUserRole(many-to-many) - Permission inheritance — roles contain permissions, users inherit permissions from roles
Role Table Structure:
MetaRole:
MetaId (Guid, PK) | Name | Description | IsSystem | Created | Modified
GUID_ROLE_1 | Administrator | Full system access | true | 2025-01-01 | 2025-01-01
GUID_ROLE_2 | Developer | Can create and modify metadata, compile code | true | 2025-01-01 | 2025-01-01
GUID_ROLE_3 | User | Can read and create data, cannot modify metadata | true | 2025-01-01 | 2025-01-01
GUID_ROLE_4 | CustomRole1 | Custom role for sales team | false | 2025-01-10 | 2025-01-10
User-Role Assignment:
MetaUserRole:
MetaId (Guid, PK) | UserMetaId | RoleMetaId | Assigned
GUID_UR_1 | GUID_USER_1 | GUID_ROLE_1 | 2025-01-01
GUID_UR_2 | GUID_USER_2 | GUID_ROLE_3 | 2025-01-02
GUID_UR_3 | GUID_USER_2 | GUID_ROLE_4 | 2025-01-10
Permission System#
Two-level permission system:
- Metadata Permissions (
MetaObjectPermission) — permissions on metadata objects (Dictionary, Document, Register, Service, etc.) - Data Permissions (
MetaDataPermission) — permissions on business data records (record-level security)
Metadata Permissions
MetaObjectPermission — controls access to metadata objects.
Permission Types:
Read— can view metadata object definitionWrite— can modify metadata object (fields, properties)Create— can create new instances of metadata objectDelete— can delete metadata objectExecute— can execute service methods, scriptsCompile— can compile code (for developers)
Permission Scope:
- Object-level — permission applies to specific metadata object (Dictionary, Document, Register, Service, etc.)
- Model-level — can grant permissions on entire model (all objects in model)
- Type-level — can grant permissions on all objects of specific type (all Dictionaries, all Documents, etc.)
Permission Grant/Deny:
IsGranted=true— grants permissionIsGranted=false— explicitly denies permission (deny overrides grant)
Example:
MetaObjectPermission:
MetaId | RoleMetaId | ObjectType | ObjectMetaId | PermissionType | IsGranted
GUID_P1 | GUID_ROLE_2 | Dictionary | GUID_DICT_1 | Write | true
GUID_P2 | GUID_ROLE_2 | Dictionary | GUID_DICT_1 | Compile | true
GUID_P3 | GUID_ROLE_3 | Dictionary | NULL | Read | true
GUID_P4 | GUID_ROLE_3 | Document | NULL | Create | true
GUID_P5 | GUID_ROLE_1 | NULL | NULL | Write | true -- Model-level: all objects
Data Permissions
MetaDataPermission — controls access to business data records (record-level security).
Permission Types:
Read— can view data recordsWrite— can modify data recordsCreate— can create new data recordsDelete— can delete data recordsPost— can post documents (specific to documents)
Permission Scope:
- Record-level — permission on specific record (
RecordIdspecified) - Table-level — permission on all records in table (
RecordId=NULL) - Condition-based — dynamic filtering via C# expression (
Conditionfield)
Assignment:
- User-based — permission assigned directly to user (
UserMetaIdspecified,RoleMetaId=NULL) - Role-based — permission assigned to role (
RoleMetaIdspecified,UserMetaId=NULL) - Combined — can have both user-specific and role-based permissions
Example:
MetaDataPermission:
MetaId | UserMetaId | RoleMetaId | TableName | RecordId | PermissionType | Condition
GUID_DP1 | NULL | GUID_ROLE_3 | Customers | NULL | Read | true
GUID_DP2 | NULL | GUID_ROLE_3 | Invoices | NULL | Create | true
GUID_DP3 | GUID_USER_2 | NULL | Invoices | 123 | Write | true -- User can edit specific invoice
GUID_DP4 | NULL | GUID_ROLE_4 | Customers | NULL | Read | "Customer.CreatedBy == CurrentUser.MetaId" -- Only own customers
GUID_DP5 | NULL | GUID_ROLE_4 | Invoices | NULL | Read | "Invoice.Status == 'Draft' OR Invoice.CreatedBy == CurrentUser.MetaId"
Condition Expressions:
- C# expressions evaluated at runtime
- Access to
CurrentUser(current user context) - Access to record fields (e.g.,
Invoice.Status,Customer.CreatedBy) - Supports complex filtering logic
Authentication#
AuthenticationService handles user authentication.
Features:
- Username/password authentication — login with username and password
- Session management — creates and manages user sessions
- Session tokens — JWT or custom tokens for API access
- Password hashing — secure password storage (bcrypt, PBKDF2, etc.)
- Session expiration — configurable session timeout
- Multiple sessions — users can have multiple active sessions
- Session tracking — IP address, user agent, last activity
Session Table Structure:
MetaSession:
MetaId (Guid, PK) | UserMetaId | SessionToken | IpAddress | UserAgent | Created | Expires | LastActivity | IsActive
GUID_SESS_1 | GUID_USER_1 | jwt_token_xyz... | 192.168.1.1 | Chrome/120 | 2025-01-15 10:00 | 2025-01-15 18:00 | 2025-01-15 14:30 | true
Authentication Flow:
- User submits username/password
AuthenticationServicevalidates credentials- If valid, creates session in
MetaSession - Returns session token to client
- Client includes token in subsequent API requests
- System validates token on each request
Authorization#
AuthorizationService handles permission checking.
Features:
- Permission checking — validates user permissions before operations
- Metadata authorization — checks
MetaObjectPermissionfor metadata operations - Data authorization — checks
MetaDataPermissionfor data operations - Role resolution — resolves user's roles from
MetaUserRole - Permission resolution — aggregates permissions from all user's roles
- Deny overrides — explicit deny overrides grants
- Condition evaluation — evaluates C# conditions in data permissions
Authorization Flow (Metadata Operation):
User attempts to modify Dictionary "Customers"
→ AuthorizationService resolves user's roles
→ Checks MetaObjectPermission for each role:
- Role1: Write on Customers? → Grant
- Role2: Write on Customers? → Deny
→ Result: DENY (deny overrides grant)
Authorization Flow (Data Operation):
User attempts to read Invoice #123
→ AuthorizationService resolves user's roles
→ Checks MetaDataPermission:
- Role permission: Read on Invoices table? → Grant
- User permission: Read on Invoice #123? → Check
- Condition: Evaluate C# expression (if exists)
→ Apply all permissions (AND logic)
→ Result: ALLOW or DENY
Condition Evaluation:
- C# expressions compiled and evaluated at runtime
- Access to:
CurrentUser— current user context (Id, Name, Roles, etc.)- Record fields — fields from the record being accessed
- Constants — system constants (current date, etc.)
- Safe execution — sandboxed to prevent security issues
Audit Logging#
MetaAuditLog — comprehensive audit trail of all user actions.
Logged Actions:
- Metadata operations — Create, Update, Delete metadata objects
- Data operations — Create, Update, Delete business data records
- Authentication — Login, Logout, Failed login attempts
- Permission changes — Role assignments, permission grants/denials
- Code execution — Script execution, compilation
Audit Log Structure:
MetaAuditLog:
MetaId | UserMetaId | Action | ObjectType | ObjectMetaId | ObjectName | TableName | RecordId | Changes (JSON) | IpAddress | Timestamp
GUID_AUD_1 | GUID_USER_1 | Create | Dictionary | GUID_DICT_1 | Customers | NULL | NULL | {"Name":"Customers"} | 192.168.1.1 | 2025-01-15 10:00
GUID_AUD_2 | GUID_USER_2 | Update | Dictionary | GUID_DICT_1 | Customers | Customers | 123 | {"Name":"Old->New"} | 192.168.1.2 | 2025-01-15 11:00
GUID_AUD_3 | GUID_USER_1 | Login | NULL | NULL | NULL | NULL | NULL | NULL | 192.168.1.1 | 2025-01-15 09:00
GUID_AUD_4 | GUID_USER_2 | Execute | Service | GUID_SVC_1 | LocationHelper | NULL | NULL | {"Method":"CalculateDistance"} | 192.168.1.2 | 2025-01-15 12:00
Changes Tracking:
- JSON field stores old and new values
- Tracks field-level changes for updates
- Complete audit trail for compliance
Audit Service Responsibilities:
- Logs all user actions automatically
- Stores IP address and user agent
- Provides audit log queries and reporting
- Supports filtering and search
- Export audit logs for compliance
Security Services#
Services:
AuthenticationService— user authentication, session managementAuthorizationService— permission checking, authorization decisionsUserService— user management (CRUD operations)PermissionService— permission management, permission queriesAuditService— audit logging, audit log queries
API Controllers:
AuthController— authentication endpoints (login, logout, token refresh)UserController— user management APIRoleController— role management APIPermissionController— permission management API
Security Middleware#
Security checks applied at multiple levels:
- API Middleware — validates session token on every API request
- Controller Authorization — checks permissions before controller actions
- Service Layer — validates permissions in service methods
- Data Layer — applies data permissions to queries (filters records)
Data Query Filtering:
- When querying data, system automatically applies data permissions
- Adds WHERE clauses based on
MetaDataPermissionconditions - Filters out records user doesn't have access to
- Transparent to application code
Example:
// User queries Invoices
var invoices = await DataService.Query<Invoice>();
// System automatically applies permissions:
// WHERE (Invoice.Status = 'Draft' OR Invoice.CreatedBy = CurrentUser.MetaId)
// (based on MetaDataPermission condition)
Built-in Roles#
System-provided roles:
-
Administrator
- Full system access
- Can manage users, roles, permissions
- Can modify all metadata and data
- Can compile code
- Cannot be revoked
-
Developer
- Can create and modify metadata
- Can write and compile code
- Can execute services and scripts
- Cannot manage users/roles (unless explicitly granted)
- Cannot delete system objects
-
User
- Can read metadata (view object definitions)
- Can create and modify data records
- Cannot modify metadata
- Cannot compile code
- Restricted by data permissions
-
Guest
- Read-only access
- Can view data (if granted)
- Cannot create or modify anything
- Limited API access
Custom Roles:
- Clients can create custom roles with specific permission sets
- Flexible permission assignment per role
🔗 DataProviders#
DataProviders provide unified access to runtime objects from external applications (Scheduler, Web).
Key features:
- Hide database-specific details
- Provide LINQ-like query interface
- Support for transactions
- Support for multiple database systems
Example usage:
var customers = context.Data.Table("Customer")
.Query()
.Where(x => x.Status == "Active")
.ToList();
🔹 Technical Implementation Details#
Dependency Injection#
Core uses dependency injection (Microsoft.Extensions.DependencyInjection) for all services.
Services available to business logic code:
IZuloOneLogger<T>— structured loggingIDataProvider— unified data access interfaceIEventBus— publish/subscribe eventsITransaction— transaction managementIHttpClient— HTTP client for external integrationsIMetadataService— metadata operations- Custom services registered in DI container
Services are registered in Startup.cs and injected into:
- Event handlers (PreInsert, PostUpdate, etc.)
- Extension classes
- DataEntity processing code
- Scheduled jobs
Entity Framework Integration#
Core uses Entity Framework Core for ORM abstraction.
Features:
- Dynamic DbContext — generated from metadata, not hardcoded
- Schema-first approach — metadata drives database schema, not code-first
- Automatic migrations — migrations generated from metadata changes
- Multi-database support — SQL Server, PostgreSQL, MySQL via EF Core providers
- No manual SQL — all SQL generated by ORM from metadata
Roslyn Integration (C# Compiler-as-a-Service)#
All dynamic code compilation uses Microsoft.CodeAnalysis (Roslyn).
Two Compilation Modes:
-
Script Compilation (CSharpScript) — for lightweight event handlers:
using Microsoft.CodeAnalysis.CSharp.Scripting; var script = CSharpScript.Create(code, globalsType: typeof(ScriptContext)); await script.RunAsync(context);- Lightweight, fast compilation
- Script code (not full classes) — a body that runs against a supplied context
- Cached in memory/Redis
- Used for event handlers (OnBeforeInsert, etc.)
-
Full Assembly Compilation — for extensions and complex logic:
using Microsoft.CodeAnalysis; var compilation = CSharpCompilation.Create(...); // Generates DLL assembly- Full class compilation
- Generates DLL assemblies
- Loaded via AssemblyLoadContext
- Used for extensions and complex business logic
Usage:
- Script compilation — compiles C# scripts stored in database to
Script<object> - Runtime compilation — compiles full C# classes to DLL assemblies
- Model generation — generates C# classes from metadata definitions
- Code analysis — validates syntax, provides diagnostics
- Language Services — provides IntelliSense, autocomplete, error detection (via LSP)
Assembly Management:
- AssemblyLoadContext — isolates loaded assemblies (plugin system)
- Used for full assembly compilation (extensions)
- Each extension can be loaded in separate context
- Allows unloading and reloading assemblies without restart
- Prevents type conflicts between different code modules
Compilation pipelines:
Events: Metadata → C# Script → CSharpScript.Compile() → Script<object> → Runtime (cached)
Extensions: Metadata → C# Source Code → Roslyn Compiler → DLL Assembly → AssemblyLoadContext → Runtime
🔄 Runtime Access for Isolated Processes#
Problem Statement#
Isolated processes (like Scheduler, workers) need access to metadata and runtime models, but they run in separate processes or domains without direct access to Core's compiled types.
Key Requirements:
- Scheduler must execute business logic that depends on metadata (tables, fields, classes, events, document posting, etc.)
- Scheduler doesn't have direct access to runtime types if Core works in separate process
- Need separation between:
- Metamodel (structure descriptions) — tables, fields, relationships, types, events
- Runtime implementation (compiled DLL or cached types) — what Core Engine creates from metamodel
Solution Architecture#
Core (ZuloOne.Core) is the single source of truth:
- Contains base metadata (in database or JSON repository)
- Generates runtime types (via Roslyn or Reflection.Emit)
- Provides API for object descriptions (JSON, ProtoBuf, or gRPC)
- Exposes Metadata Packages for offline deployment
Isolated Processes (Scheduler, Workers) connect to Core via:
- Option A: HTTP/gRPC API (Core.API)
- Option B: Shared database (direct metadata read)
- Option C: Metadata Package (ZIP/JSON) for offline operation
Option A: Through Core API (Modern & Flexible)#
Architecture:
Core → /api/metadata endpoints → Scheduler/Workers
Core → /api/runtime endpoints → Runtime type information
Core → EventBus (RabbitMQ/Kafka) → MetadataChanged events
Implementation:
- Core exposes REST/gRPC API (
/api/metadata/*and/api/runtime/*) - Scheduler requests metadata on startup (e.g.,
GET /api/metadata/tables) - Scheduler reconstructs local structures via Reflection or dynamic compilation
- On metadata changes, Core publishes
MetadataChangedevent (via RabbitMQ/Kafka) - Scheduler receives event and updates its local cache
Pros:
- ✅ Modularity and scalability
- ✅ Single version of Core
- ✅ Centralized metadata management
- ✅ Real-time updates via events
Cons:
- ❌ Requires Core API always online
- ❌ Network dependency
API Endpoints:
GET /api/metadata/tables— all table definitionsGET /api/metadata/runtime/types/{typeName}— compiled type infoPOST /api/metadata/subscribe— subscribe to MetadataChanged eventsGET /api/metadata/package— download metadata package
Option B: Through Shared Metadata Repository (Simple & Fast)#
Architecture:
Shared Database (SQL)
├── MetaDictionary
├── MetaDictionaryProperty
├── MetaDocumentType
├── MetaDocumentHeaderField
├── MetaDocumentLineField
├── MetaDocumentStatus
├── MetaRegister
├── MetaRegisterHeader
├── MetaRegisterField
└── MetaScript (all scripts: events, extensions, DataEntities, jobs, tests, etc.)
Core ← Reads/Writes
Scheduler ← Reads directly
Implementation:
- All metadata stored in shared database tables (
MetaDictionary,MetaDictionaryProperty,MetaDocumentHeaderField,MetaDocumentLineField,MetaDocumentStatus,MetaRegisterHeader,MetaRegisterField,MetaScript, etc.) - Scheduler reads metadata directly from database on startup
- Scheduler builds its own runtime types locally (via Roslyn compilation)
- Metadata versioning handled in database (version column)
Pros:
- ✅ Simple and fast (direct DB access)
- ✅ No network dependency on Core API
- ✅ Can version metadata in database
- ✅ Works even if Core is offline
Cons:
- ❌ Direct database coupling
- ❌ More code needed for complex logic (event hooks, etc.)
- ❌ Need to handle metadata changes detection
Database Schema:
MetaDictionary: Id, Name, Description, ModelId, Version
MetaDictionaryProperty: Id, DictionaryId, FieldName, EDTId, IsRequired, ...
MetaDocumentType: Id, Name, ModelId, ...
MetaDocumentHeaderField: Id, DocumentTypeId, FieldName, EDTId, ...
MetaDocumentLineField: Id, DocumentTypeId, FieldName, EDTId, ...
MetaDocumentStatus: Id, DocumentTypeId, StatusName, StatusValue, WorkflowOrder, IsInitial, IsFinal, ...
MetaRegister: Id, Name, ModelId, ...
MetaRegisterHeader: Id, RegisterId, FieldName, EDTId, ...
MetaRegisterField: Id, RegisterId, FieldName, FieldType, EDTId, ...
MetaScript: Id, Name, ScriptType, ObjectName, EventName, Code, ModelId, Version
Option C: Through Metadata Package (Offline Mode)#
Architecture:
Core → Generates ZIP Package → Scheduler checks periodically
Package contains:
├── metadata.json (all metadata definitions)
├── compiled.dll (pre-compiled runtime types)
└── version.json
Implementation:
- Core generates Metadata Package when metadata changes:
- JSON descriptions (metadata.json)
- Compiled DLL with runtime types (compiled.dll)
- Version information
- Scheduler periodically checks for package updates (or listens for events)
- If changed, Scheduler reloads runtime model from package
- Scheduler can work offline using cached package
Pros:
- ✅ Offline operation capability
- ✅ Easy testing and deployment
- ✅ Version-controlled packages
- ✅ Can distribute to multiple nodes
Cons:
- ❌ Slight delay on updates (until package is checked)
- ❌ Need package generation and distribution mechanism
Package Structure:
MetadataPackage-v1.0.0.zip
├── metadata.json // All metadata definitions
├── compiled.dll // Pre-compiled runtime types
├── version.json // Version info
└── manifest.json // Package metadata
ZuloOne.Runtime Shared Library#
For unified access, create ZuloOne.Runtime library that can:
Capabilities:
- Load metadata from API, database, or file
- Generate runtime types via Roslyn
- Execute business logic (Post, Insert, Events, etc.)
- Manage AssemblyLoadContext for hot-reload
- Cache compiled types and assemblies
Usage:
- Web interface, Scheduler, CLI tools all reference
ZuloOne.Runtime - Unified API for metadata and runtime access
- Consistent behavior across all components
ZuloOne.Runtime Structure:
ZuloOne.Runtime
├── Metadata/
│ ├── MetadataLoader.cs // Load from API/DB/file
│ ├── MetadataRepository.cs // Metadata storage abstraction
│ └── MetadataVersion.cs // Version tracking
├── Runtime/
│ ├── TypeGenerator.cs // Generate types from metadata
│ ├── RuntimeLoader.cs // Load compiled assemblies
│ └── AssemblyManager.cs // AssemblyLoadContext management
├── Execution/
│ ├── ScriptEngine.cs // Execute event scripts
│ ├── ExtensionExecutor.cs // Execute extensions
│ └── DataProvider.cs // Unified data access
└── Services/
├── MetadataService.cs // Metadata operations
└── RuntimeService.cs // Runtime type operations
Example Usage:
// In Scheduler or Worker
var runtime = new ZuloOneRuntime();
await runtime.InitializeAsync(
source: MetadataSource.Api, // or Database, Package
endpoint: "http://core-api/metadata"
);
// Access metadata
var customerTable = runtime.Metadata.GetTable("Customer");
// Access runtime types
var customerType = runtime.Runtime.GetType("Customer");
var instance = Activator.CreateInstance(customerType);
// Execute business logic
await runtime.ExecuteEvent("Customer", "OnBeforeInsert", instance);
Runtime Domain Loader (Hot-Reload)#
RuntimeDomainLoader component that:
Features:
- Loads assemblies (extensions, events) into separate
AssemblyLoadContext - Allows "hot" reloading when code changes
- Scheduler doesn't need restart on logic changes
- Isolates different versions of assemblies
Implementation:
public class RuntimeDomainLoader
{
private readonly Dictionary<string, AssemblyLoadContext> _contexts = new();
public void LoadAssembly(string name, byte[] assemblyBytes)
{
var context = new AssemblyLoadContext(name, isCollectible: true);
context.LoadFromStream(new MemoryStream(assemblyBytes));
_contexts[name] = context;
}
public void UnloadAssembly(string name)
{
if (_contexts.TryGetValue(name, out var context))
{
context.Unload();
_contexts.Remove(name);
}
}
public void ReloadAssembly(string name, byte[] newAssemblyBytes)
{
UnloadAssembly(name);
LoadAssembly(name, newAssemblyBytes);
}
}
Usage in Scheduler:
- Core publishes
MetadataChangedevent with new assembly bytes - Scheduler's RuntimeDomainLoader receives event
- Unloads old assembly from AssemblyLoadContext
- Loads new assembly
- No restart needed — hot reload complete
Recommended Approach: Hybrid#
Primary: Option B (Shared Database) for reliability Secondary: Option A (API) for real-time updates Fallback: Option C (Package) for offline scenarios
Implementation:
Scheduler Startup:
1. Try Option B (read from shared DB) - fastest
2. Subscribe to Option A (API events) for real-time updates
3. Fallback to Option C (cached package) if DB/API unavailable
Plugin System (AssemblyLoadContext)#
Architecture:
- Each compiled module (extension, event handler) is a plugin
- Plugins loaded via
AssemblyLoadContextfor isolation - Plugins can be:
- Dynamically loaded at runtime
- Hot-reloaded without restart
- Unloaded when no longer needed
- Prevents type conflicts between different plugins
Plugin lifecycle:
- Plugin code stored in metadata (as C# source)
- Compiled via Roslyn into assembly
- Loaded into
AssemblyLoadContext - Registered in system
- Can be reloaded when code changes
📝 MVP Goals#
- Core with reading
MetaDictionaryandMetaDictionaryProperty - EDT support — Extended Data Types with parameters
- MetaDictionary — dictionaries
- MetaDocumentType — document type definitions with header and lines
- Reference EDT automatic relationship — automatic reference column and UI dropdown (no FK constraints in DB)
- Business Logic Extensions — extension classes stored in MetaScript (ScriptType="ExtensionClass") with inheritance and method overriding (
base()calls) - EF model and DataContext generation
- API for CRUD on metadata
- Support for Pre/Post events via Roslyn
- Database schema generation from EDT and metadata objects
- Extension compilation and hot-reload
📖 See Also#
- Database Schema Documentation - detailed database schema
- Scheduler Architecture - scheduler architecture