ZuloOne
English
Sign in
Menu

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:

  1. 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, ...
  2. MetaEDT (Extended Data Type) — extended data types with parameters (length, precision, scale, etc.)
    • Fields: MetaId (Guid, primary key, auto-generated), Name, EDTType, Length, Precision, ...
  3. MetaDictionary — dictionaries - simple lookup tables
    • Fields: MetaId (Guid, primary key, auto-generated), Name, ModelId, LayerId, IsExtension, BaseDictionaryMetaId, ...
    • Extension support: IsExtension=true, BaseDictionaryMetaId references base dictionary MetaId
  4. MetaDictionaryProperty — field definitions for dictionaries (separate table)
    • Fields: MetaId (Guid, primary key, auto-generated), DictionaryMetaId (references MetaDictionary.MetaId), FieldName, EDTMetaId, IsRequired, etc.
  5. 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, BaseDocumentTypeMetaId references base document type MetaId
  6. 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)
  7. 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, BaseDocumentLineMetaId references base line type MetaId
    • System automatically generates C# class for line type in metadata with properties like DocumentType, LineNumber, etc.
  8. 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 MetaDocumentHeaderField table)
    • Extension support: When document line type is extended, new line field definitions are added to this table with DocumentLineMetaId pointing to extension line type
  9. 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)
  10. 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, BaseRegisterMetaId references base register MetaId
  1. 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, ...
  2. 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, ...
  3. 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, BaseEnumMetaId references base enum MetaId
  4. MetaEnumValue — enum value definitions (text-value pairs)

    • Fields: MetaId (Guid, primary key, auto-generated), EnumMetaId (references MetaEnum.MetaId), Name, Value (int), DisplayOrder, Description
  5. 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, BaseDataEntityMetaId references base DataEntity MetaId
  6. 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 MetaScript with ScriptType="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
  7. 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)
  8. Event Handlers — each metadata object (Dictionary, Document, Register) automatically gets one event handler class stored in MetaScript:

    • MetaScript.ScriptType = "EventHandler" — identifies as event handler class
    • MetaScript.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
  9. Business Logic Extensions — stored in MetaScript with ScriptType="ExtensionClass"

    • MetaScript.Name — extension class name (e.g., "InvoicePostingExt")
    • MetaScript.Code — full extension class C# code
    • MetaScript.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"
  10. 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
  11. 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
  12. 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
  13. Tests — unit and integration tests stored in MetaScript table

    • Test scripts stored in MetaScript with ScriptType="UnitTest" or ScriptType="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
  14. MetaPackage — installed package registry (tracks package versions, dependencies)

  15. MetaScriptHistory — script version history and audit trail

  16. MetaJob — job definitions for Scheduler (references MetaScript)

  17. MetaUser — user accounts (system users)

    • Fields: MetaId (Guid, PK), UserName, Email, PasswordHash, IsActive, FirstName, LastName, LastLogin, Created, Modified, ...
  18. MetaRole — security roles (groups of permissions)

    • Fields: MetaId (Guid, PK), Name, Description, IsSystem (system roles cannot be deleted), Created, Modified, ...
  19. MetaUserRole — many-to-many relationship between users and roles

    • Fields: MetaId (Guid, PK), UserMetaId (references MetaUser.MetaId), RoleMetaId (references MetaRole.MetaId), Assigned, ...
  20. MetaPermission — permission definitions (types of permissions)

    • Fields: MetaId (Guid, PK), Name, PermissionType (MetadataRead, MetadataWrite, DataRead, DataWrite, Execute, Compile, etc.), Description, ...
  21. 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), ...
  22. 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), ...
  23. MetaSession — user sessions (active user sessions)

    • Fields: MetaId (Guid, PK), UserMetaId (references MetaUser.MetaId), SessionToken, IpAddress, UserAgent, Created, Expires, LastActivity, IsActive, ...
  24. 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, ...

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:

  1. Core — core business logic (base classes, common functionality)

    • Base document types
    • Core dictionaries
    • System registers
    • Cannot be modified by clients (read-only)
  2. Tax — tax calculation and reporting model

    • Tax-related documents
    • Tax registers
    • Tax calculation logic
  3. CRM — customer relationship management model

    • Customer dictionaries
    • Sales documents
    • Customer-related registers
  4. 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
  • Decimal/Float EDT — decimal number with precision and scale

    • Parameters: Length (total digits), Precision (decimal places)
    • Database: DECIMAL(length, precision) or NUMERIC(length, precision)

String EDT:

  • String EDT — text field with length
    • Parameters: Length (max characters), IsUnicode, IsFixedLength
    • Database: VARCHAR(length), NVARCHAR(length), CHAR(length), NCHAR(length)

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
  • Date EDT — date only (without time)

    • Parameters: DateOnly = true
    • Database: DATE
    • Use when time component is not needed
  • 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) or BINARY(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), BLOB or LONGBLOB (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

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:
      1. Stores integer enum value (0, 1, 2, etc.) in the field
      2. Creates a dropdown control in UI with enum text labels
      3. Compiles enum type for type-safe code access
      4. Runtime provides enum value → text conversion
  • Database mapping:

    • Field stores int value (enum underlying type)
    • UI displays text label from MetaEnumValue
  • 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 reference
    • ReferenceFieldName — field to display in dropdown (usually "Name" or "Code")
  • Automatic behavior:

    • When a field uses a Reference EDT, the system automatically:
      1. Stores reference Id value in the field (no Foreign Key constraint in database)
      2. Creates a dropdown control in UI with reference data
      3. Relationship logic is handled by the system, not by database constraints
      4. No additional configuration needed - relationship is defined by the EDT in the field

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) — uses MetaId (Guid, PK) as primary key
  • Business table (Customers, Addresses, etc.) — uses Id (PK) as primary key for business records
  • When creating field reference: business table column stores Id of referenced business record, not MetaId

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: Customers table has Id (PK), Code, Name, AddressId (references Addresses.Id)
  • 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.MetaId of base dictionary (if IsExtension=true)
  • Extension creates new dictionary record with IsExtension=true and BaseDictionaryMetaId pointing 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 MetaDictionaryProperty table
  • 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:

  • MetaEnum table — stores enum definitions
  • MetaEnumValue table — 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 enum
  • BaseEnumId — 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 MetaEnum by 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:

  • MetaDocumentType table — stores document type definitions

    • Fields: MetaId (Guid, PK), Name, Description, NumberingSequence, etc.
  • MetaDocumentHeaderField table — 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 DocumentTypeMetaId pointing to extension document type
  • MetaDocumentLine table — 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, BaseDocumentLineMetaId references base line type MetaId
  • MetaDocumentLineField table — 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 MetaDocumentHeaderField table)
    • Extension support: When document line type is extended, new line field definitions are added to this table with DocumentLineMetaId pointing to extension line type
  • MetaDocumentStatus table — 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"

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 MetaDocumentHeaderField records
    • Standard fields: Id (PK), Number, Date, Status, etc.
    • Reference fields: CustomerId (references Customers.Id), etc.
    • Note: Business tables use Id, not MetaId (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
  • 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 MetaDocumentLineField records (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 MetaDocumentStatus table (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.MetaId of base document type (if IsExtension=true)
  • Extension creates new document type record with IsExtension=true and BaseDocumentTypeMetaId pointing 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 MetaDocumentHeaderField table
  • Line types stored in MetaDocumentLine table
  • Line fields stored in MetaDocumentLineField table (references MetaDocumentLine.MetaId)
  • Statuses stored in MetaDocumentStatus table
  • 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):

  • MetaDocumentStatus table — defines possible statuses for each document type
  • Workflow order — WorkflowOrder field defines status transition sequence
  • Initial status — IsInitial=true flag marks the status document starts with (typically "Draft")
  • Final status — IsFinal=true flag 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 MetaDocumentStatus records
  • 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.BuildChainAsync scopes the transaction chain by subtype. A null subtype narrows to nothing and pulls in chains belonging to other states — on a postOnSave type, 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 → Received on a type that declares Draft → 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:

  • MetaRegister table — 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.MetaId for custom engine behavior
    • Belongs to a Model (ModelId)
    • Belongs to a Layer (LayerId)
    • Extension support: IsExtension=true, BaseRegisterMetaId references base register MetaId
  • MetaRegisterHeader table — 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)
  • MetaRegisterField table — 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 MetaRegisterHeader table)
    • Field definitions edited directly in register editor (not separate page)

Extension Support:

  • IsExtension — boolean flag indicating if this register extends another
  • BaseRegisterMetaId — references MetaRegister.MetaId of base register (if IsExtension=true)
  • Extension creates new register record with IsExtension=true and BaseRegisterMetaId pointing 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 MetaRegisterHeader records
    • Standard fields: Id (PK), Period, Dimension1, Dimension2, etc.
    • Note: Business tables use Id, not MetaId (MetaId is only for metadata objects)
  • Lines table ({RegisterName}Lines) — register lines/rows (business records)

    • Fields generated from MetaRegisterField records (where FieldType="Line")
    • Standard fields: Id (PK), RegisterId (references header.Id), Resource, Quantity, etc.

🔧 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:

  1. Receipt document posts → adds to FIFO queue (with cost, quantity, date)
  2. Issue document posts → consumes from queue (oldest first)
  3. Balance query → calculates remaining queue items
  4. 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:

  1. Transaction posts → adds Debit and Credit
  2. After posting → engine checks balance (Debit - Credit)
  3. If balance = 0 → automatically removes record
  4. 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:

  1. Any change → creates history record
  2. Stores timestamp, user, old value, new value
  3. Query → returns all history records (no aggregation)

5. Custom Engine

Custom engine with developer-defined behavior:

  • EngineScriptId references MetaScript with 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 implementation
  • FIFORegisterEngine — FIFO implementation
  • FinancialRegisterEngine — financial collapsing implementation
  • FlatHistoryRegisterEngine — flat history implementation
  • CustomRegisterEngine — 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 — with ScriptType="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:

  • MetaService table — stores service definitions

    • Fields: MetaId (Guid, PK), Name, Description, ModelId, LayerId, Namespace, Author, Modified, Version
  • MetaScript table — 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, ...

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 or IsComputed (filled by MapFromSource)
  • Events — TypedDataEntityEventHandler<T> (DataEntityEvents script)

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 name
  • EDTMetaId — reference to MetaEDT.MetaId (defines type and parameters)
  • IsRequired — mandatory field
  • IsUnique — unique constraint
  • DefaultValue — default value
  • ValidationRules — 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 fields
  • Int — integer numbers
  • Decimal — decimal numbers
  • DateTime — date and time
  • Boolean — true/false values
  • Reference — foreign key (now via Reference EDT)
  • Enum — enumeration reference
  • Collection — one-to-many collection
  • Computed — 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 ObjectMetaId field 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 code
  • ExtensionClass — full extension class code
  • ServiceClass — service class (helper classes for business logic, no visual UI)
  • JobScript — scheduled job code
  • ValidationScript — custom validation logic
  • RegisterEngineScript — 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 class
  • MetaScript.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 methods
  • MetaScript.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 MetaScript table
  • 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 processed
  • Services (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 record
  • OnAfterInsert — after inserting a new record
  • OnBeforeUpdate — before updating a record
  • OnAfterUpdate — after updating a record
  • OnBeforeDelete — before deleting a record
  • OnAfterDelete — after deleting a record
  • OnValidateField — field-level validation (fieldName parameter available)
  • OnBeforePost — before posting document (for documents)
  • OnAfterPost — after posting document (for documents)

Runtime Execution Mechanism:

When system starts:

  1. Loads all metadata objects and their associated events from database
  2. Compiles scripts using CSharpScript.Create() with ScriptContext as globals type
  3. Caches compiled scripts in memory dictionary (or Redis for multi-instance)
  4. 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 ScriptContext available
  • 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 MetaScriptHistory table
  • 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:
    1. Bug detected in production after release
    2. Identify problematic metadata object(s)
    3. View version history — find last known working version
    4. Compare versions — see what changed between working and broken version
    5. Execute rollback — revert to last working version
    6. System automatically:
      • Restores metadata object to previous version
      • Recompiles affected scripts if needed
      • Updates runtime types
      • Invalidates affected caches
    7. 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:

  1. Full Assembly Compilation — for extensions and complex business logic (full classes)
  2. Script Compilation — for lightweight event handlers (CSharpScript API)

Script Compilation (CSharpScript):

  • Uses Microsoft.CodeAnalysis.CSharp.Scripting for 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:

  1. Reads metadata definitions (including MetaEnum and MetaEnumValue)
  2. 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")
  3. Compiles source via Roslyn into assembly (DLL)
  4. Loads assembly using AssemblyLoadContext (plugin system)
  5. Caches assembly for reuse
  6. On metadata change: invalidates cache, regenerates, recompiles

Enum Compilation Process:

  1. Reads MetaEnum records (base enums and extensions)
  2. For each enum:
    • Reads MetaEnumValue records (sorted by DisplayOrder)
    • If extension (IsExtension=true), merges with base enum values
    • Generates C# enum code:
      public enum EnumNoYes
      {
          No = 0,
          Yes = 1
      }
      
  3. Compiles enum types into assembly alongside entity models
  4. 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 — logging
    • IDataProvider — data access
    • IEventBus — event publishing
    • ITransaction — transaction management
    • IHttpClient — 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 DbContext class 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 MetaScript table (where ScriptType="EventScript")
  • Resolves script code from MetaScript table via ScriptId reference
  • 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:

  1. Event triggered (e.g., OnBeforeInsert for Customer)
  2. EventService queries MetaScript table:
    SELECT * FROM MetaScript 
    WHERE ObjectName = 'Customer' 
      AND EventName = 'OnBeforeInsert' 
      AND ScriptType = 'EventScript'
      AND IsActive = true
    
  3. Gets compiled script from cache (or compiles if not cached)
  4. Creates ScriptContext with Entity and Services
  5. Executes compiled script with context
  6. Handles exceptions (validation errors, etc.)
  7. Logs execution result

ExtensionService#

Manages business logic extension classes registration and execution (stored in MetaScript).

Responsibilities:

  • Loads extension definitions from MetaScript table where ScriptType="ExtensionClass"
  • Uses BaseClassName field to identify base class being extended
  • Resolves extension class code from MetaScript.Code field
  • 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 CustomRegisterEngine from MetaScript (via EngineScriptId)
  • 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 (when RegisterEngineType = "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:

  1. Document posts → generates register movements
  2. RegisterEngineService resolves register's engine type from MetaRegister
  3. Gets or creates engine instance (from cache or factory)
  4. Calls engine.Post(document, movements)
  5. 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 update required
  • Ensures database is always in sync with metadata

Migration Flow on Startup:

  1. Core starts → MigrationService initializes
  2. Reads all metadata from database
  3. Compares with current database schema
  4. Generates migration scripts for differences
  5. Applies migrations automatically
  6. 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 MetaProject records
  • Manages MetaProjectObject table (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 ProjectId are 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 MetaScript table (where ScriptType="UnitTest" or ScriptType="IntegrationTest")
  • Tests are regular scripts in MetaScript table (not separate table):
    • ScriptType="UnitTest" or ScriptType="IntegrationTest"
    • ObjectType="Test"
    • ObjectName — test name
    • ObjectMetaId — 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:

  1. Upload ZIP package via API/UI
  2. Validate package structure and manifest
  3. Check dependencies and conflicts
  4. Create installation backup
  5. Import metadata (EDT, Dictionaries, Documents, etc.)
  6. Compile and load code (Extensions, Events, DataEntities)
  7. Apply database migrations
  8. Verify installation
  9. Record package version


🌐 API Controllers#

DataController#

RESTful API for data operations:

  • GET /api/data/{tableName} — query records
  • POST /api/data/{tableName} — create record
  • PUT /api/data/{tableName}/{id} — update record
  • DELETE /api/data/{tableName}/{id} — delete record

MetadataController#

RESTful API for metadata management:

  • GET /api/metadata/tables — list all tables
  • POST /api/metadata/tables — create table definition
  • PUT /api/metadata/tables/{name} — update table definition
  • GET /api/metadata/tables/{name}/fields — get table fields

EventController#

API for event management:

  • POST /api/events/{tableName}/test — test event handler
  • GET /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 project
  • GET /api/projects/{id} — get project details
  • PUT /api/projects/{id} — update project
  • DELETE /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 ZIP
  • POST /api/projects/{id}/promote — promote project to production (removes MetaProjectObject entries)
  • POST /api/projects/{id}/merge — merge with another project
  • GET /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 test
  • GET /api/tests/{id} — get test details
  • PUT /api/tests/{id} — update test
  • DELETE /api/tests/{id} — delete test
  • POST /api/tests/{id}/run — run specific test
  • POST /api/tests/run-project/{projectId} — run all tests in project
  • POST /api/tests/run-object/{objectId} — run tests for specific object
  • GET /api/tests/{id}/results — get test execution results
  • GET /api/tests/coverage/{projectId} — get test coverage for project

PackageController#

API for package management:

  • POST /api/packages/upload — upload ZIP package
  • POST /api/packages/{id}/install — install package
  • GET /api/packages — list installed packages
  • GET /api/packages/{id} — get package details
  • POST /api/packages/{id}/rollback — rollback to previous version
  • POST /api/packages/export — export current configuration as package
  • POST /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 tables
  • GET /api/metadata/tables/{name} — get table definition
  • GET /api/metadata/tables/{name}/fields — get table fields
  • GET /api/metadata/runtime/types — get compiled runtime type information
  • GET /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 definitions
  • MetadataService.GetTable(string name) — get specific table
  • RuntimeService.GetTypeInfo(string typeName) — get runtime type information
  • MetadataService.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=true and Base*Id pointing 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 MetaScript and 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

🏗️ 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:

  1. System Layer (Order=0)

    • Core system objects
    • Base dictionaries, documents, registers
    • Cannot be modified by ISV or Customer
    • Example: Dictionary "CustomerTable" in CRM model
  2. 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"
  3. 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 LayerId field — determines which layer the model belongs to
  • Objects inherit layer from their model — LayerId inherited from MetaModel.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 LayerId for overrides (optional), but typically inherit from model

Extension Chain Compilation#

Compilation Order:

  1. System Layer (Order=0) — compile all base objects
  2. ISV Layer (Order=1) — compile ISV extensions (reference System)
  3. 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:

  1. Developer selects base object (e.g., Dictionary "CustomerTable")
  2. Clicks "Create Extension" button or dropdown action
  3. System creates new object record in same table (e.g., new MetaDictionary record "CustomerTableExt")
  4. System sets IsExtension=true and BaseDictionaryId="CustomerTable" (or BaseDocumentTypeId, BaseRegisterId, BaseDataEntityId for other types)
  5. Extension inherits all fields from base (shown read-only in editor, resolved via Base*Id chain)

Extension Chain Resolution:

  • System follows Base*Id chain: Extension → Base → Base.Base → ...
  • For Dictionary: follows BaseDictionaryId chain
  • For Document: follows BaseDocumentTypeId chain
  • For Register: follows BaseRegisterId chain
  • For DataEntity: follows BaseDataEntityId chain

Scripts and Events:

  • Scripts stored in MetaScript and linked to extension object (via ObjectName field)
  • 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 identifier
  • Name — extension class name (e.g., "InvoicePostingExt")
  • ScriptType = "ExtensionClass" — identifies as extension class
  • ObjectName — extension class name (same as Name)
  • BaseClassName — base class being extended (e.g., "InvoicePosting")
  • Code — full C# code of extension class
  • ModelId — model this extension belongs to
  • IsActive — 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:

  1. System checks for extensions of InvoicePosting
  2. 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
  3. 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 MetaScript table with ScriptType="ExtensionClass"
  • MetaScript.BaseClassName identifies which base class is extended
  • MetaScript.Code contains full extension class code
  • All extension metadata and code in single MetaScript record
  • 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 MetaProjectObject table (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:

  1. Upload Package

    • User uploads ZIP file through Web UI
    • System validates ZIP structure
    • Extracts and validates manifest.json
  2. Validation

    • Check package version compatibility
    • Verify dependencies (required packages)
    • Detect conflicts with existing metadata
    • Validate code syntax
  3. Backup

    • Create snapshot of current state
    • Backup metadata to allow rollback
    • Record current package versions
  4. Installation

    • Import metadata (EDT, Dictionaries, Documents, etc.)
    • Compile C# code (Extensions, Events, DataEntities)
    • Apply database migrations (if any)
    • Load compiled assemblies
  5. Verification

    • Verify all metadata imported correctly
    • Test code compilation
    • Validate database schema
    • Check for errors
  6. 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) or VARCHAR(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

Automatic behavior:

  • When Reference EDT is used in a field, system automatically:
    1. Creates reference column ({FieldName}Id) storing the referenced table's Id
    2. No Foreign Key constraint is created in database
    3. Relationship integrity is maintained by application logic
    4. Creates index on reference column for performance (optional, configurable)

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)
  • Reads MetaDocumentType to get document type definition
  • Reads MetaDocumentHeaderField records (filtered by DocumentTypeMetaId) to generate header table fields
  • Reads MetaDocumentLine records (filtered by DocumentTypeMetaId) to get all line types for document
  • For each line type:
    • Reads MetaDocumentLineField records (filtered by DocumentLineMetaId) to generate line table fields
    • Creates runtime table: {LineTypeName} (e.g., "SalesOrderLines" for default line type "SalesOrderLine", "InvoiceDiscountLines" for additional line type)
  • Creates header table: {DocumentTypeName} (e.g., "SalesOrder")
  • Links lines to header via DocumentId column (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.ScriptId for document-specific logic

Register Structure Generation#

For registers (header + lines):

  • Reads MetaRegister to get register type definition
  • Reads MetaRegisterHeader records (filtered by RegisterId) to generate header table fields
  • Reads MetaRegisterField records (filtered by RegisterId and FieldType="Line") to generate lines table fields
  • Creates two runtime tables: {RegisterName} (header) and {RegisterName}Lines (lines)
  • Links lines to header via RegisterId column (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.ScriptId for 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 — IsActive flag to enable/disable accounts
  • Last login tracking — LastLogin timestamp
  • 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:

  1. Metadata Permissions (MetaObjectPermission) — permissions on metadata objects (Dictionary, Document, Register, Service, etc.)
  2. 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 definition
  • Write — can modify metadata object (fields, properties)
  • Create — can create new instances of metadata object
  • Delete — can delete metadata object
  • Execute — can execute service methods, scripts
  • Compile — 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 permission
  • IsGranted=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 records
  • Write — can modify data records
  • Create — can create new data records
  • Delete — can delete data records
  • Post — can post documents (specific to documents)

Permission Scope:

  • Record-level — permission on specific record (RecordId specified)
  • Table-level — permission on all records in table (RecordId=NULL)
  • Condition-based — dynamic filtering via C# expression (Condition field)

Assignment:

  • User-based — permission assigned directly to user (UserMetaId specified, RoleMetaId=NULL)
  • Role-based — permission assigned to role (RoleMetaId specified, 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:

  1. User submits username/password
  2. AuthenticationService validates credentials
  3. If valid, creates session in MetaSession
  4. Returns session token to client
  5. Client includes token in subsequent API requests
  6. System validates token on each request

Authorization#

AuthorizationService handles permission checking.

Features:

  • Permission checking — validates user permissions before operations
  • Metadata authorization — checks MetaObjectPermission for metadata operations
  • Data authorization — checks MetaDataPermission for 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 management
  • AuthorizationService — permission checking, authorization decisions
  • UserService — user management (CRUD operations)
  • PermissionService — permission management, permission queries
  • AuditService — audit logging, audit log queries

API Controllers:

  • AuthController — authentication endpoints (login, logout, token refresh)
  • UserController — user management API
  • RoleController — role management API
  • PermissionController — permission management API

Security Middleware#

Security checks applied at multiple levels:

  1. API Middleware — validates session token on every API request
  2. Controller Authorization — checks permissions before controller actions
  3. Service Layer — validates permissions in service methods
  4. 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 MetaDataPermission conditions
  • 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:

  1. Administrator

    • Full system access
    • Can manage users, roles, permissions
    • Can modify all metadata and data
    • Can compile code
    • Cannot be revoked
  2. 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
  3. User

    • Can read metadata (view object definitions)
    • Can create and modify data records
    • Cannot modify metadata
    • Cannot compile code
    • Restricted by data permissions
  4. 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 logging
  • IDataProvider — unified data access interface
  • IEventBus — publish/subscribe events
  • ITransaction — transaction management
  • IHttpClient — HTTP client for external integrations
  • IMetadataService — 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:

  1. 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.)
  2. 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:

  1. Core exposes REST/gRPC API (/api/metadata/* and /api/runtime/*)
  2. Scheduler requests metadata on startup (e.g., GET /api/metadata/tables)
  3. Scheduler reconstructs local structures via Reflection or dynamic compilation
  4. On metadata changes, Core publishes MetadataChanged event (via RabbitMQ/Kafka)
  5. 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 definitions
  • GET /api/metadata/runtime/types/{typeName} — compiled type info
  • POST /api/metadata/subscribe — subscribe to MetadataChanged events
  • GET /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:

  1. All metadata stored in shared database tables (MetaDictionary, MetaDictionaryProperty, MetaDocumentHeaderField, MetaDocumentLineField, MetaDocumentStatus, MetaRegisterHeader, MetaRegisterField, MetaScript, etc.)
  2. Scheduler reads metadata directly from database on startup
  3. Scheduler builds its own runtime types locally (via Roslyn compilation)
  4. 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:

  1. Core generates Metadata Package when metadata changes:
    • JSON descriptions (metadata.json)
    • Compiled DLL with runtime types (compiled.dll)
    • Version information
  2. Scheduler periodically checks for package updates (or listens for events)
  3. If changed, Scheduler reloads runtime model from package
  4. 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:

  1. Core publishes MetadataChanged event with new assembly bytes
  2. Scheduler's RuntimeDomainLoader receives event
  3. Unloads old assembly from AssemblyLoadContext
  4. Loads new assembly
  5. No restart needed — hot reload complete

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 AssemblyLoadContext for 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:

  1. Plugin code stored in metadata (as C# source)
  2. Compiled via Roslyn into assembly
  3. Loaded into AssemblyLoadContext
  4. Registered in system
  5. Can be reloaded when code changes

📝 MVP Goals#

  1. Core with reading MetaDictionary and MetaDictionaryProperty
  2. EDT support — Extended Data Types with parameters
  3. MetaDictionary — dictionaries
  4. MetaDocumentType — document type definitions with header and lines
  5. Reference EDT automatic relationship — automatic reference column and UI dropdown (no FK constraints in DB)
  6. Business Logic Extensions — extension classes stored in MetaScript (ScriptType="ExtensionClass") with inheritance and method overriding (base() calls)
  7. EF model and DataContext generation
  8. API for CRUD on metadata
  9. Support for Pre/Post events via Roslyn
  10. Database schema generation from EDT and metadata objects
  11. Extension compilation and hot-reload

📖 See Also#

Was this page helpful?