Schemas and Elements in TypeScript

A Schema represents an ECSchema in TypeScript. It is a collection of Entity-based classes. See the BIS overview for how ECSchemas are used to model information. ECSchemas define classes for models, elements, and aspects, as well as ECRelationships.

An Element object represents an instance of a bis:Element class when it is read from an iModel. The Element object has properties that correspond to the bis class definition in the schema. An ElementAspect object represents an instance of a bis:Aspect in memory.

ECSchemas typically define subclasses of bis:Element, bis:Aspect, and so on. The objects that are loaded into memory are instances of TypeScript/JavaScript classes that match the ECSchema definitions. So, for example, an instance of a bis:GeometricElement3d is an object of the class GeometricElement3d.

Importing the Schema

An ECSchema must be imported into an iModel before apps can insert and query instances of the ECClasses that it defines.

Example:


// Import the RobotWorld schema into the specified iModel.
// Also do some one-time bootstrapping of supporting definitions such as Categories.
public static async importSchema(requestContext: ClientRequestContext | AuthorizedClientRequestContext, iModelDb: IModelDb): Promise<void> {
  requestContext.enter();
  if (iModelDb.containsClass(_schemaNames.Class.Robot))
    return;

  if (iModelDb.isReadonly)
    throw new IModelError(IModelStatus.ReadOnly, "importSchema failed because IModelDb is read-only");

  // Must import the schema. The schema must be installed alongside the app in its
  // assets directory. Note that, for portability, make sure the case of
  // the filename is correct!
  await iModelDb.importSchemas(requestContext, [path.join(IModelHost.appAssetsDir!, "RobotWorld.ecschema.xml")]);
  requestContext.enter();

  // This is the right time to create definitions, such as Categories, that will
  // be used with the classes in this schema.
  RobotWorld.bootStrapDefinitions(iModelDb);
}

ECSchema.xml files must be in the app backend's install set, as part of its assets.

The app can ensure that the underlying schema is imported by registering an onOpened event handler:

Example:

// Make sure the RobotWorld schema is in the iModel.
BriefcaseDb.onOpened.addListener((requestContext: AuthorizedClientRequestContext | ClientRequestContext, iModel: IModelDb) => {
  RobotWorld.importSchema(requestContext, iModel); // eslint-disable-line @typescript-eslint/no-floating-promises
});

where the schema is:

<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="RobotWorld" alias="RobotWorld" version="01.00.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.1">

    <ECSchemaReference name="BisCore" version="01.00" alias="bis"/>
    <ECSchemaReference name="ECDbMap" version="02.00" alias="ecdbmap"/>

    <ECEntityClass typeName="Robot" modifier="Sealed">
        <BaseClass>bis:SpatialLocationElement</BaseClass>
        <ECProperty propertyName="radius" typeName="double" description="the girth of the robot" />
    </ECEntityClass>

    <ECEntityClass typeName="Barrier" modifier="Sealed">
        <BaseClass>bis:SpatialLocationElement</BaseClass>
        <ECProperty propertyName="length" typeName="double" description="the length of the barrier" />
    </ECEntityClass>

    <!-- A Barrier can optionally have a hole init -->
    <ECEntityClass typeName="BarrierHoleAspect">
        <BaseClass>bis:ElementUniqueAspect</BaseClass>
        <ECCustomAttributes>
            <ClassHasHandler xmlns="BisCore.01.00"/>
        </ECCustomAttributes>
        <ECProperty propertyName="offset" typeName="double" description="offset to start of hole"/>
        <ECProperty propertyName="length" typeName="double" description="length of hole"/>
    </ECEntityClass>

</ECSchema>

TypeScript and ECSchemas and ECClasses

Once an ECSchema has been imported into an iModel, you can work with Elements, Models, and ElementAspects from that schema without writing TypeScript classes to represent them. A JavaScript class will be generated dynamically to represent each ECClass that you access, if there is no pre-registered TypeScript class to represent it.

You may write a TypeScript Schema class to represent an ECSchema and TypeScript Element-based or ElementAspect-based classes to represent some or all of its ECClasses. The benefit of writing a TypeScript class to represent an ECClass is that you can add hand-coded methods to provide and centralize business logic for applications to use when working with that specific class.

Example:

import { IModelDb, SpatialCategory, SpatialLocationElement } from "@bentley/imodeljs-backend";
import { GeometryStreamBuilder, GeometryStreamProps } from "@bentley/imodeljs-common";
import { RobotWorld } from "./RobotWorldSchema";

/**
 * An example of defining a subclass of SpatialLocationElement.
 * Normally, you would start writing a class like this by generating the TypeScript class
 * definition from the schema. Then, you would then hand-edit it to add methods.
 * In this example, a "robot" is represented as a circle in the X-Y plane.
 */
export class Robot extends SpatialLocationElement {
  public static override get className(): string { return "Robot"; }
  //  Define the properties added by this subclass
  public radius: number = 0.1;                     // The girth of the robot

  // Note: Do not redefine the constructor. You must not interfere with the constructor that is
  // already defined by the base Element class.

  // You can provide handy methods for creating new Robots
  public static generateGeometry(radius: number = 0.1): GeometryStreamProps {
    const builder = new GeometryStreamBuilder();  // I know what graphics represent a robot.
    const circle = Arc3d.createXY(Point3d.createZero(), radius);
    builder.appendGeometry(circle);
    return builder.geometryStream;
  }

  public static getCategory(iModel: IModelDb): SpatialCategory {
    return RobotWorld.getCategory(iModel, RobotWorld.Class.Robot);
  }

  // You can write methods to implement business logic that apps can call.
  public someBusinessLogic(): void {
    if (this.radius < 12.34) {
      // ... do something ...
    }
  }
}

Note that the pre-written TypeScript class does not have to define accessors for the properties of the ECClass. The Element base class takes care of that automatically.

Note that you still have to import the underlying ECSchema before attempting to create instances of the ECClasses that it defines.

Schema Registration

If an app backend wants to use a pre-written TypeScript Schema class, it must first register it and all of the classes that it defines. The best practice is for the Schema class to do that in its constructor.

Example:

// Import all modules that define classes in this schema.
import * as robots from "./RobotElement";

// ... other modules ...

/** An example of defining a class that represents a schema.
 * Important: The name of the TypeScript class must match the name of the ECSchema that it represents.
 * Normally, you would use a tool to generate a TypeScript schema class like this from an ECSchema
 * definition. You would then edit the generated TypeScript class to add methods.
 */
export class RobotWorld extends Schema {
  public static override get schemaName(): string { return "RobotWorld"; }
  /** An app must call this to register the RobotWorld schema prior to using it. */
  public static registerSchema() {

    // Make sure that this Schema is registered.
    // An app may call this more than once. Make sure that's harmless.
    if (this !== Schemas.getRegisteredSchema(RobotWorld.name)) {
      Schemas.registerSchema(this);
      ClassRegistry.registerModule(robots, this);
      ClassRegistry.registerModule(obstacles, this);
    }
  }

Last Updated: 11 June, 2024