-
Notifications
You must be signed in to change notification settings - Fork 0
Rules Engine Architecture
The AAS Generator (MnestixCore/AASGenerator/) is a specialized component within the Mnestix Backend that enables automated creation of AAS Submodels from structured JSON data using template-based rules. This component was developed as part of Luis Schweinberger's Bachelor thesis on rules engines for Asset Administration Shells (see thesis on GitHub).
The AAS Generator solves the Industry 4.0 challenge of transforming existing structured data into standardized digital twins (AAS). Instead of manual AAS creation, it provides:
- Automated Transformation: JSON data → AAS Submodel instances
- Template-Based Rules: Embedded mapping rules within AAS templates
- Complex Mappings: Beyond 1:1 field mapping (collections, filtering, conditions)
- AAS Compliance: Generated Submodels conform to AAS Metamodel v3.x
-
Main Interface:
IAasGenerator(AASGenerator/Interfaces/IAasGenerator.cs) - Primary service interface, implemented byAasGenerator(AASGenerator/AASGenerator.cs) -
REST Endpoint:
POST /api/v2/DataIngest/{base64EncodedAasId}- HTTP API for generation requests (v1 is deprecated) - Integration: Called by other Mnestix components for automated Submodel creation
- AasGenerator: Orchestrates the entire generation process
- SubmodelDataToInstanceMapper: Coordinates the transformation pipeline
- BlueprintProvider: Manages template storage and retrieval
- Pipeline Steps: Individual transformation operations (10 steps)
Uses Pipes-and-Filters pattern (MnestixCore/Shared/Pipeline/) with 10 sequential steps:
-
ValidateBlueprint - Runs the shared
BlueprintValidatoragainst the blueprint; aborts early with structured errors if validation fails (defense-in-depth for blueprints imported outside the API) - DeepCloneBlueprint - Creates a working copy of the blueprint so the original is never mutated
-
SetKindInstance - Changes
kindfromTemplatetoInstance -
DuplicateCollections - Expands
SMT/CollectionMappingInfoqualifiers (replicates child elements for each array item) -
FilterElements - Evaluates
SMT/FilterMappingInfoJsonata boolean expressions and removes elements that fail -
DiscoverMappingDescriptors - Finds all
SMT/MappingInfoqualifiers, parses field names, resolves cardinality, and builds aMappingDescriptorlist for downstream steps -
ResolveMappingExpressions - Evaluates each descriptor's JSONata expression against the data; enforces mandatory cardinality; populates
ResolvedMappings -
AssignMappedFields - Iterates resolved mappings and delegates to the
FieldAssignerRegistry(see Field Assigners below); logs a warning when template defaults are overridden -
RemoveTopLevelQualifiers - Strips template-only qualifiers (
SMT/…) from the generated instance - ReplaceIdentification - Assigns the new Submodel ID to the instance
Context Object: DataMappingContext carries immutable inputs (blueprint, data, language, submodel ID) and mutable state (MappingDescriptors, ResolvedMappings, SubmodelInstance) through all steps
Extensibility: New rule types implement IPipelineStep<DataMappingContext> and register in PipelineBuilder
The context object (DataMappingContext) is the single shared state that flows through all pipeline steps. It separates immutable inputs from mutable working state:
Immutable inputs (set at construction):
| Field | Type | Purpose |
|---|---|---|
Blueprint |
JObject |
The original blueprint template (never mutated) |
Data |
JObject |
The user-provided JSON payload |
Language |
string? |
Language code for MLP value mappings (e.g. "en") |
NewSubmodelId |
string |
ID assigned to the generated instance |
BlueprintValidator |
IBlueprintValidator |
Shared validator for defense-in-depth checks |
Mutable state (populated by pipeline steps):
| Field | Type | Written by |
|---|---|---|
SubmodelInstance |
JObject |
DeepCloneBlueprint (initial), then mutated by all downstream steps |
MappingDescriptors |
List<MappingDescriptor> |
DiscoverMappingDescriptors |
ResolvedMappings |
List<ResolvedMapping> |
ResolveMappingExpressions |
Qualifier |
JToken |
Updated by steps to track the currently processed qualifier (for error context) |
Logging:
-
Log(message)/LogInfo(message)— appends an INFO entry -
LogWarning(message)— appends a WARNING entry -
Logs— accumulated log trail (shared with theWorkflowLogger)
Supporting types:
-
MappingDescriptor— describes a single discovered qualifier: the target element, field name, JSONata expression, cardinality (mandatory/optional), model type, and the original qualifier token -
ResolvedMapping— pairs a descriptor with its evaluatedJToken?result (null = not found in data)
The AAS Generator uses the Pipes-and-Filters pattern based on Buschmann et al.'s design:
Template Input → [Step1] → [Step2] → [Step3] → ... → [StepN] → Submodel Instance
│ │ │ │
▼ ▼ ▼ ▼
Context Context Context Context
Benefits:
- Clear separation of concerns per transformation step
- Easy extensibility for new rule types
- Robust error handling with context preservation
- Individually testable components
Pipeline Builder Pattern: Steps registered fluently in DataMapper.cs
Step 8 (AssignMappedFields) delegates value assignment to specialized FieldAssignerBase subclasses via FieldAssignerRegistry:
| Assigner | Field | Behaviour |
|---|---|---|
ValueFieldAssigner |
value |
Model-type-aware: MLP scalar→lang array (requires language param); Property/Blob/File validates valueType conformance |
MultiLanguageFieldAssigner |
multiLanguage |
Expects JObject with language keys; produces lang array |
IdShortFieldAssigner |
idShort |
Sanitizes to AAS v3 pattern [a-zA-Z][a-zA-Z0-9_]*
|
DisplayNameFieldAssigner |
displayName |
Find-or-add by language in the displayName array |
FirstFieldAssigner |
first |
AAS Reference object for RelationshipElement |
SecondFieldAssigner |
second |
AAS Reference object for RelationshipElement |
DefaultFieldAssigner |
(any other) | Fallback: element[field] = value.ToString()
|
All assigners log a warning when the target field already holds a non-empty template default that is being overridden by mapped data. This gives visibility into cases where partial defaults are silently replaced.
- Input: Blueprint (JObject), Data (JObject), Language (string), NewSubmodelId (string), WorkflowLogger
-
Context Creation:
DataMappingContextinitialized with inputs and sharedBlueprintValidator - Sequential Processing: Each step modifies context and passes to next
-
Output: Generated Submodel instance in
context.SubmodelInstance - Error Handling: Pipeline halts on first error, preserves full context and logs up to the failure point
Rules are stored as Template Qualifiers directly within AAS Submodel templates.
Template Qualifier Format:
{
"type": "SMT/<RuleType>",
"value": "<rule-configuration>"
}Purpose: Set static values directly in templates
Qualifier: None required - values set directly in template element
Result: Value copied unchanged to instance
Purpose: 1:1 mapping from JSON paths OR advanced Jsonata expressions to element values
Qualifier: SMT/MappingInfo
Examples:
- Simple path:
"value": "car.serialNo"mapsdata.car.serialNoto element value - String function:
"value": "$uppercase(car.code)"transforms to uppercase - Numeric conversion:
"value": "$string(quantity)"converts number to string - Boolean expression:
"value": "car.price > 1000"returns true/false - Chained operations:
"value": "car.email ~> $substringAfter('@')"extracts domain Implementation:ResolveMappingExpressionsStep+AssignMappedFieldsStep(uses Jsonata.Net.Native library)
Jsonata Reference: See Blueprint and Rules for complete function list
Purpose: Duplicate elements for each array item
Qualifier: SMT/CollectionMappingInfo
Example: "value": "car.contacts[*]" creates N elements for N contacts
Implementation: DuplicateCollectionsStep.cs (see algorithm comments)
Result: contactPerson_0, contactPerson_1, etc. with mapped child values
Purpose: Create elements only when conditions are met
Qualifier: SMT/FilterMappingInfo
Status: ✅ Implemented - Uses Jsonata boolean expressions
Example: "value": "car.engineType = 'electric'" creates element only for electric cars
Implementation: FilterElementsStep.cs
Syntax: Supports Jsonata boolean operators (=, !=, >, <, >=, <=, and, or, in)
Purpose: Define behavior when referenced data is missing
Qualifier: SMT/Cardinality
Values: "One" / "OneToMany" (mandatory, throws error if missing) | "ZeroToOne" / "ZeroToMany" (optional, empty value + warning if missing). A value is treated as mandatory when it starts with "One".
Implementation: Checked in ResolveMappingExpressionsStep (mandatory → exception, optional → warning + skip)
JSONata-style syntax:
-
data.field- Simple access -
data.nested.field- Nested objects -
data.array[*]- Array placeholder (collections) -
data.array[0]- Specific index (after processing)
The AAS Generator includes comprehensive Jsonata expression support for advanced data transformations beyond simple path navigation.
String Functions:
-
$length(str)- Character count -
$substring(str, start, length)- Extract substring -
$contains(str, pattern)- Check if contains (returns boolean) -
$uppercase(str),$lowercase(str)- Case conversion -
$trim(str)- Remove whitespace -
$split(str, sep),$join(array, sep)- String/array operations -
$replace(str, old, new)- Text replacement
Numeric Functions:
-
$number(value)- Convert to number -
$string(value)- Convert to string -
$abs(num),$floor(num),$ceil(num)- Math functions -
$round(num, precision)- Rounding -
$power(base, exp),$sqrt(num)- Advanced math
Comparison & Boolean Operations:
-
=,!=,>,<,>=,<=- Comparisons (return boolean) -
and,or- Logical operators -
in- Array membership
Pipe Operator:
-
data.value ~> $function($)- Pass result to next function
Boolean Expressions:
-
field = 'value'- Equality check -
field != 'value'- Inequality check -
numA > numB- Numeric comparison -
field and otherfield- Logical AND -
field or otherfield- Logical OR -
value in ['a', 'b', 'c']- Array membership
String Transformation:
{
"type": "SMT/MappingInfo",
"value": "$substring(code, 0, 3) ~> $uppercase($)"
}Input: "code": "abc123" → Output: "ABC"
Type Conversion:
{
"type": "SMT/MappingInfo",
"value": "$string(quantity)"
}Input: "quantity": 42 → Output: "42"
Boolean Check:
{
"type": "SMT/MappingInfo",
"value": "email ~> $contains('@')"
}Input: "email": "user@example.com" → Output: true
Filter Expression:
{
"type": "SMT/FilterMappingInfo",
"value": "vehicle.engineType = 'electric' and vehicle.year >= 2020"
}Element created only if both conditions are true
-
Blueprint validation failure:
BlueprintValidationExceptionwith list ofBlueprintValidationError(structured, all issues reported) -
Missing mandatory data:
SubmodelDataToInstanceMapperExceptionwith context - Missing optional data: Warning logged, field skipped
-
ValueType mismatch:
SubmodelDataToInstanceMapperExceptionwith expected vs actual type - Structured errors: Include qualifier, path, and processing context
The AAS Generator includes a workflow-level logging system that captures a chronological log trail across the entire Submodel generation lifecycle. This provides full observability into the generation process for debugging and error diagnosis.
WorkflowLogger (MnestixCore/AASGenerator/WorkflowLogger.cs) is a lightweight dual-write logger that:
-
Accumulates log entries in an
IList<string>for inclusion in API responses -
Forwards each entry to the injected
ILoggerat the appropriate log level
A new WorkflowLogger instance is created per blueprint in AddDataToAasAsync, ensuring each blueprint has an independent log trail.
All entries follow the convention: SEVERITY [timestamp] - message where timestamps use the ISO-8601 round-trip format ("O" specifier):
INFO [2026-04-24T10:30:01.0000000Z] - Mapping blueprint contact-template-v1 to AAS aHR0cHM6...
INFO [2026-04-24T10:30:01.1000000Z] - Fetching blueprint: contact-template-v1
INFO [2026-04-24T10:30:01.2000000Z] - Blueprint fetched successfully
ERROR [2026-04-24T10:30:01.3000000Z] - Data mapping failed: ...
This matches the format used by the existing DataMappingContext pipeline logs, so all log entries are consistent when merged.
Each phase of AddDataToAasAsync is instrumented:
| Phase | Log Entries |
|---|---|
| Context Preamble | Optional caller-provided preamble (e.g. Created a new AAS with aasId {aasId} from AasCreator), then Mapping blueprint {id} to AAS {aasId}
|
| Blueprint Retrieval |
Fetching blueprint: {id}, Blueprint fetched successfully
|
| IdShort Extraction | Extracted idShort: {idShort} |
| ID Generation |
Generating submodel ID, Submodel ID generated: {id}
|
| Data Mapping |
Starting data mapping, pipeline step logs (merged via AddRange), Data mapping completed
|
| Repository Persistence |
Posting submodel to repository, Adding submodel reference to shell, Submodel reference added to shell
|
-
debug=true+ success:DebugInfo.Logscontains the full log trail from all phases -
debug=false+ success:DebugInfoisnull(no logs returned) -
Error (any
debugvalue):ErrorInfo.Logsalways contains the log trail up to and including the failure point — this aids error diagnosis without requiring the caller to opt into debug mode
- SubmodelElementList: Partial support
-
MultiLanguageProperty: Single language per call when using
SMT/MappingInfo/value; useSMT/MappingInfo/multiLanguagefor multi-language in one call -
Default override semantics: When mapped data is provided, it fully replaces any template default value — partial merges are not supported (e.g. template default
[{en:"Default"}]+ data{de:"Nur Deutsch"}→ result[{de:"Nur Deutsch"}]only). A warning is logged when this happens. - Complex expressions: Advanced Jsonata features (aggregation, conditionals) not fully supported
POST /api/v2/DataIngest/{base64EncodedAasId}
{
"blueprintsIds": ["contact-template-v1"],
"data": {
"contacts": [
{"name": "John Doe", "email": "john@example.com"}
]
},
"language": "en"
}Templates are AAS Submodels with kind: "Template" and embedded Template Qualifiers. Created via Template Builder UI or direct API.
Two mapping approaches are supported:
-
SMT/MappingInfo/multiLanguage(recommended) — maps a JSON object with language keys (e.g.{"en": "Hello", "de": "Hallo"}) to a full lang array in one call. Nolanguagerequest parameter needed. -
SMT/MappingInfo/value(legacy) — maps a scalar and wraps it with thelanguageparameter from the API request. Only one language per generation call.
Override semantics: When mapped data is provided, the entire element["value"] array is replaced — any pre-existing template default entries are dropped. A warning is logged when a non-empty default is overridden. This is intentional: templates that rely on partial defaults should be aware that providing any data replaces all defaults.