Workspaces and Settings

Every non-trivial application requires some level of configuration to customize its run-time behavior and help it locate data resources required for it to perform its functions. An iTwin.js Workspace comprises the Settings that supply this configuration and the WorkspaceContainers that provide those resources. Settings inside of Workspace.settings provide values for individual SettingNames, some of which point to one or more WorkspaceDbs that provide binary resources for particular purposes. The anatomy of a Workspace is illustrated below:

graph LR
  subgraph Workspace
    S["Settings
(priority-ordered stack)"] subgraph Settings Containers SC1["SettingsDb
iTwin scope"] SC2["SettingsDb
iModel scope"] end subgraph Workspace Containers WC1["WorkspaceDb
fonts, textures"] WC2["WorkspaceDb
templates"] end end SC1 -->|"loads as
SettingsDictionary"| S SC2 -->|"loads as
SettingsDictionary"| S S -->|"settings point to"| WC1 S -->|"settings point to"| WC2

Settings are stored in SettingsDb containers (cloud-hosted, versioned, and discoverable by containerType: "settings"). Binary resources like fonts, textures, and images are stored in WorkspaceDb containers. At runtime, each SettingsDb becomes one SettingsDictionary in the Settings priority stack, and those settings tell the application where to find the WorkspaceDbs it needs.

To explore Workspace concepts, let's take the example of an imaginary application called "LandscapePro" that allows users to decorate an iModel by adding landscaping features like trees, shrubs, flower beds, and patio furniture.

Settings

Settings are how administrators of an application or project configure the workspace for end-users. Be careful to avoid confusing them with "user preferences", which can be configured by individual users. For example, an application might provide a check box to toggle "dark mode" on or off. Each individual user can make their own choice as to whether they want to use this mode - it is a user preference, not a setting. But an administrator may define a setting that controls whether users can see that check box in the first place.

A Setting is simply a name-value pair. The value can be of one of the following types:

  • A string, number, or boolean;
  • An object containing properties of any of these types; or
  • An array containing elements of one of these types.

A SettingName must be unique, 1 to 1024 characters long with no leading nor trailing whitespace, and should begin with the schema prefix of the schema that defines the setting. For example, LandscapePro might define the following settings:

"landscapePro/ui/defaultToolId" "landscapePro/ui/availableTools" "landscapePro/flora/preferredStyle" "landscapePro/flora/treeDbs" "landscapePro/hardinessRange"

Each setting's name begins with the "landscapePro" schema prefix followed by a forward slash. Forward slashes are used to create logical groupings of settings, similar to how file paths group files into directories. In the above example, "ui" and "flora" are two separate groups containing two settings each, while "hardinessRange" is a top-level setting. An application user interface that permits the user to view or edit settings would probably present these groups as individual nodes in a tree view, or as tabs.

Settings schemas

The metadata describing a group of related Settings is defined in a SettingGroupSchema. The schema is based on JSON Schema, with the following additions:

  • schemaPrefix (required) - a unique name for the schema. All of the names in the schema inherit this prefix.
  • description (required) - a description of the schema appropriate for displaying to a user.
  • settingDefs - an object consisting of SettingSchemas describing individual Settings, indexed by their SettingNames.
  • typeDefs - an object consisting of SettingSchemas describing reusable types of Settings that can be referenced by SettingSchemas in this or any other schema.
  • order - an optional integer used to sort the schema in a user interface that lists multiple schemas, where schemas of lower order sort before those with higher order.

We can define the LandscapePro schema programmatically as follows:

const schema: SettingGroupSchema = { schemaPrefix: "landscapePro", description: "LandscapePro configuration settings", settingDefs: { "flora/preferredStyle": { type: "string", description: "The name of one of a set of predefined 'styles' of foliage that might be used to select appropriate flora", }, "flora/treeDbs": { type: "array", extends: "itwin/core/workspace/workspaceDbList", }, "ui/defaultTool": { type: "string", description: "Id of the tool that is active when the application starts", }, "ui/availableTools": { type: "array", description: "Ids of tools that should be shown to the user", items: { type: "string", }, combineArray: true, }, "hardinessRange": { type: "object", description: "Specifies the upper and lower limits on the hardiness zone for flora that can survive in a region", properties: { minimum: { type: "integer", extends: "landscapePro/hardinessZone", description: "The lower limit on hardiness zone for flora that can survive in a region", }, maximum: { type: "integer", extends: "landscapePro/hardinessZone", description: "The upper limit on hardiness zone for flora that can survive in a region", }, }, }, }, typeDefs: { hardinessZone: { type: "integer", description: "A USDA hardiness zone used to describe the surivability of plants in a geographical region", minimum: 0, maximum: 13, }, }, };

This schema defines 5 settingDefs and 1 typeDef. Note the "landscapePro" schema prefix, which is implicitly included in the name of each settingDef and typeDef in the schema - for example, the full name of the "hardinessRange" setting is "landscapePro/hardinessRange".

The "hardinessZone" typeDef represents a USDA hardiness zone as an integer between 0 and 13. The "hardinessRange" settingDef reuses that typeDef for both its "minimum" and "maximum" properties by declaring that each extends that type. Note that extends requires the schema prefix to be specified, even within the same schema that defines the typeDef.

The "flora/treeDbs" settingDef extends the "workspaceDbList" typeDef from a different schema - the workspace schema delivered with the application, with the "itwin/core/workspace" schema prefix.

Registering schemas

Schemas enable the application to validate that the setting values loaded at run-time match the expected types - for example, if we try to retrieve the value of the "landscapePro/ui/defaultToolId" setting and discover a number where we expect a string, an exception will be thrown. They can also be used by user interfaces that allow administrators to configure settings by enforcing types and other constraints like the one that requires "hardinessZone" to be an integer between 0 and 13. To do this, the schema must first be registered.

The set of currently-registered schemas can be accessed via IModelHost.settingsSchemas. You can register new ones in a variety of ways. Most commonly, applications will deliver their schemas in JSON files, in which case they can use SettingsSchemas.addFile to supply a single JSON file or SettingsSchemas.addDirectory to supply a directory full of them. In our case, however, we've defined the schema programmatically, so we'll register it using SettingsSchemas.addGroup:

IModelHost.settingsSchemas.addGroup(schema);

Your application should register its schemas shortly after invoking IModelHost.startup. Registering a schema adds its typeDefs and settingDefs to SettingsSchemas.typeDefs and SettingsSchemas.settingDefs, respectively. It also raises the SettingsSchemas.onSchemaChanged event. All schemas are unregistered when IModelHost.shutdown is invoked.

Settings dictionaries

The values of Settings are provided by SettingsDictionarys. The Settings for the current session can be accessed via the settings property of IModelHost.appWorkspace. You can add new dictionaries to provide settings values at any time during the session, although most dictionaries will be loaded shortly after IModelHost.startup.

Let's load a settings dictionary that provides values for some of the settings in the LandscapePro schema:

const values: SettingsContainer = { "landscapePro/ui/defaultTool": "place-shrub", "landscapePro/ui/availableTools": ["place-shrub", "place-koi-pond", "apply-mulch"], }; const props: SettingsDictionaryProps = { // A unique name for this dictionary. name: "LandscapeProDefaults", // This dictionary's priority relative to other dictionaries. priority: SettingsPriority.defaults, }; IModelHost.appWorkspace.settings.addDictionary(props, values);

Now you can access the setting values defined in the dictionary via IModelHost.appWorkspace.settings:

let defaultTool = IModelHost.appWorkspace.settings.getString("landscapePro/ui/defaultTool"); // "place-shrub" let availableTools = IModelHost.appWorkspace.settings.getArray<string>("landscapePro/ui/availableTools"); // ["place-shrub", "place-koi-pond", "apply-mulch"] let preferredStyle = IModelHost.appWorkspace.settings.getString("landscapePro/flora/preferredStyle"); // undefined const preferredStyleOrDefault = IModelHost.appWorkspace.settings.getString("landscapePro/flora/preferredStyle", "default"); // "default"

Note that getString returns undefined for "landscapePro/preferredStyle" because our dictionary didn't provide a value for it. The overload of that function (and similar functions like Settings.getBoolean and Settings.getObject) allows you to specify a default value to use if the value is not defined.

Note: In general, avoid caching the values of individual settings - just query them each time you need them, because they can change at any time. If you must cache (for example, if you are populating a user interface from the setting values), listen for and react to the Settings.onSettingsChanged event.

Any number of dictionaries can be added to Workspace.settings. Let's add another one:

IModelHost.appWorkspace.settings.addDictionary({ name: "LandscapeProOverrides", priority: SettingsPriority.application, }, { "landscapePro/flora/preferredStyle": "coniferous", "landscapePro/ui/defaultTool": "place-koi-pond", "landscapePro/ui/availableTools": ["place-gazebo", "apply-mulch"], });

This dictionary adds a value for "landscapePro/flora/preferredStyle", and defines new values for the two settings that were also defined in the previous dictionary. See what happens when we look up those settings' values again:

defaultTool = IModelHost.appWorkspace.settings.getString("landscapePro/ui/defaultTool"); // "place-koi-pond" availableTools = IModelHost.appWorkspace.settings.getArray<string>("landscapePro/ui/availableTools"); // ["place-gazebo", "apply-mulch", "place-shrub", "place-koi-pond"] preferredStyle = IModelHost.appWorkspace.settings.getString("landscapePro/flora/preferredStyle"); // "coniferous"

Now, as expected, "landscapePro/flora/preferredStyle" is no longer undefined. The value of "landscapePro/ui/defaultTool" has been overwritten with the value specified by the new dictionary. And the "landscapePro/ui/availableTools" array now has the merged contents of the arrays defined in both dictionaries. What rules determine how the value of a setting is resolved when multiple dictionaries provide a value for it? The answer lies in the dictionaries' SettingsPrioritys.

Settings priorities

Configurations are often layered: an application may ship with built-in default settings, that an administrator may selectively override for all users of the application. Beyond that, additional configuration may be needed on a per-organization, per-iTwin, and/or per-iModel level. SettingsPriority defines which dictionaries' settings take precedence over others - the dictionary with the highest priority overrides any other dictionaries that provide a value for a given setting.

A SettingsPriority is just a number, but specific values carry semantics:

SettingsDictionarys of application priority or lower reside in IModelHost.appWorkspace. iTwin-scoped settings are loaded into the workspace returned by IModelHost.getITwinWorkspace — see iTwin settings. Settings of even higher priority (branch and iModel) are stored in an IModelDb.workspace — see iModel settings.

iTwin settings

So far, we have been working with IModelHost.appWorkspace. But - as mentioned above - each iTwin has its own workspace as well, with its own Settings that can override and/or supplement the application workspace's settings. These settings are stored as named SettingsDictionarys in a WorkspaceDb scoped to that iTwin. Whenever IModelHost.getITwinWorkspace is called, all named dictionaries in the container are loaded into the returned Workspace.settings at SettingsPriority.iTwin. An application working in the context of a particular iTwin should resolve setting values by asking IModelHost.getITwinWorkspace, which will fall back to IModelHost.appWorkspace if the iTwin's setting dictionaries don't provide a value for the requested setting.

Before using iTwin settings, ensure two services are configured:

With those in place, load the iTwin workspace:

const iTwinWorkspace = await IModelHost.getITwinWorkspace(iTwinId); const defaultView = iTwinWorkspace.settings.getString("myApp/defaultView");

The returned Workspace gives you access to all settings and resources associated with the iTwin.

Saving iTwin settings

To save a named settings dictionary for an iTwin, call IModelHost.saveSettingDictionary with a dictionary name and a SettingsContainer of key-value pairs:

IModelHost.settingsSchemas.addGroup({ schemaPrefix: "myApp", description: "MyApp settings", settingDefs: { defaultView: { type: "string" }, maxDisplayedItems: { type: "integer" }, }, }); await IModelHost.saveSettingDictionary(iTwinId, "myApp/settings", { "myApp/defaultView": "plan", "myApp/maxDisplayedItems": 100, });

If no settings container exists for the specified iTwin yet, one is created automatically. The dictionary name becomes the resource name in the WorkspaceDb. Multiple named dictionaries can coexist in the same container.

Deleting iTwin settings

To remove an entire named dictionary, use IModelHost.deleteSettingDictionary:

await IModelHost.deleteSettingDictionary(iTwinId, "myApp/settings");

Reading iTwin settings

To read iTwin settings, query the settings of the Workspace returned by IModelHost.getITwinWorkspace:

const workspace = await IModelHost.getITwinWorkspace(iTwinId); const defaultViewFromRead = workspace.settings.getString("myApp/defaultView"); const maxItems = workspace.settings.getNumber("myApp/maxDisplayedItems");

iTwin settings are loaded with SettingsPriority.iTwin priority.

iModel settings

Each IModelDb has its own Workspace, accessible via IModelDb.workspace. This workspace inherits all app-level and iTwin-level settings, and layers on settings stored inside the iModel itself. Because these iModel-level dictionaries are loaded at SettingsPriority.iModel — the highest built-in priority — they override any lower-priority setting with the same name.

Use iModel settings when a particular iModel needs configuration that differs from the rest of its iTwin, or when you want to persist metadata (like an iTwin settings container reference) inside the iModel so it is available in future sessions.

Saving iModel settings

To save settings into an iModel, call IModelDb.saveSettingDictionary with a dictionary name and a SettingsContainer of key-value pairs:

interface HardinessRange { minimum: number; maximum: number; } const range: HardinessRange = { minimum: 6, maximum: 8 }; await iModel.acquireSchemaLock(); await withEditTxn(iModel, async (txn) => txn.saveSettingDictionary("landscapePro/iModelSettings", { "landscapePro/hardinessRange": range, }));

The dictionary name (e.g. "landscapePro/iModelSettings") identifies the dictionary within the iModel. If a dictionary with that name already exists, it is replaced; otherwise a new one is created. You can save multiple dictionaries under different names.

Deleting iModel settings

To remove an entire settings dictionary from an iModel, use IModelDb.deleteSettingDictionary:

iModel.deleteSettingDictionary("landscapePro/iModelSettings");

Reading iModel settings

Settings saved into the iModel are automatically loaded into IModelDb.workspace. Read them the same way you read any other settings — the priority stack resolves the effective value:

const hardinessRange = iModel.workspace.settings.getObject<HardinessRange>("landscapePro/hardinessRange"); // returns { minimum: 8, maximum: 10 } defaultTool = iModel.workspace.settings.getString("landscapePro/ui/defaultTool"); // returns "place-koi-pond" as specified by IModelHost.appWorkspace.settings.

In the example above, landscapePro/hardinessRange was saved into the iModel, so it is returned from the iModel's dictionary. But landscapePro/ui/defaultTool was not saved at the iModel level, so it falls through to the app-level dictionary that defined it earlier.

Overriding iTwin settings per iModel

Because SettingsPriority.iModel is higher than SettingsPriority.iTwin, any setting saved at the iModel level takes precedence over the same setting at the iTwin level.

For example, suppose the iTwin setting landscapePro/flora/preferredStyle is "naturalistic" for the entire iTwin, but one particular iModel represents a formal garden:

// The iTwin setting says "naturalistic", but this iModel is a formal garden. await withEditTxn(iModel, async (txn) => txn.saveSettingDictionary("landscapePro/iModelOverrides", { "landscapePro/flora/preferredStyle": "formal", }));

Now when the application reads landscapePro/flora/preferredStyle from this iModel's workspace, it gets "formal". All other iModels in the iTwin continue to use the iTwin-level value.

Referencing iTwin settings from an iModel

An iModel doesn't inherently know which iTwin settings container it should use. By saving the container's identity as an iModel-level setting, the iTwin settings become part of the iModel's workspace — so IModelDb.workspace becomes the single place to resolve all settings and resources the iModel uses.

const iTwinWorkspaceForModelRef = await IModelHost.getITwinWorkspace(iTwinId); const settingsSourcesForModelRef = iTwinWorkspaceForModelRef.settingsSources; assert(undefined !== settingsSourcesForModelRef); await withEditTxn(iModel, async (txn) => txn.saveSettingDictionary("landscapePro/iModelSettings", { "landscapePro/itwinSettingsRef": settingsSourcesForModelRef, }));

The next time the iModel is opened, your app reads landscapePro/itwinSettingsRef and uses it to load the same iTwin settings container. By default this resolves to the latest version of those settings.

Pinning iTwin settings versions

If you need to pin the iModel to a specific version of the iTwin settings — so that its configuration does not change even when the iTwin settings are updated — save the version alongside the container props:

// Pin the iModel to the current version of the iTwin settings. // The settingsSources already include the version of the settings WorkspaceDb; // saving that version into the iModel locks it to that snapshot. const pinnedSettingsSources = Array.isArray(settingsSourcesForModelRef) ? settingsSourcesForModelRef : [settingsSourcesForModelRef]; await withEditTxn(iModel, async (txn) => txn.saveSettingDictionary("landscapePro/iModelSettings", { "landscapePro/itwinSettingsRef": pinnedSettingsSources, }));

Workspace resources

We've now covered settings — the name-value pairs that configure an application's behavior. But applications also depend on resources: binary data files like fonts, textures, images, and templates. This section explains how resources are stored and accessed.

The kinds of resources vary widely, but common examples include:

  • GeographicCRSes used to specify an iModel's spatial coordinate system.
  • Images that can be used as pattern maps for Textures.

While you could technically store resources as Setting values, doing so would present significant disadvantages:

  • Some resources, like images and fonts, may be defined in a binary format that is inefficient to represent using JSON.
  • Some resources, like geographic coordinate system definitions, must be extracted to files on the local file system before they can be used.
  • Some resources may be large, in size and/or quantity.
  • Resources can often be reused across many projects, organizations, and iModels.
  • Administrators often desire for resources to be versioned.
  • Administrators often want to restrict who can read or create resources.

To address these requirements, workspace resources are stored in immutable, versioned CloudSqlite databases called WorkspaceDbs, and Settings are configured to enable the application to locate those resources in the context of a session and - if relevant - an iModel.

A WorkspaceDb can contain any number of resources of any kind, where "kind" refers to the purpose for which it is intended to be used. For example, fonts, text styles, and images are different kinds of resources. Each resource must have a unique name, between 1 and 1024 characters in length and containing no leading or trailing whitespace. A resource name should incorporate a schemaPrefix and an additional qualifier to distinguish between different kinds of resources stored inside the same WorkspaceDb. For example, a database might include text styles named "itwin/textStyles/styleName" and images named "itwin/patternMaps/imageName". Prefixes in resource names are essential unless you are creating a WorkspaceDb that will only ever hold a single kind of resource.

Ultimately, each resource is stored as one of three underlying types:

  • A string, which quite often is interpreted as a serialized JSON object. Examples include text styles and settings dictionaries.
  • A binary blob, such as an image.
  • An embedded file, like a PDF file that users can view in a separate application.

String and blob resources can be accessed directly using WorkspaceDb.getString and WorkspaceDb.getBlob. File resources must first be copied onto the local file system using WorkspaceDb.getFile, and should be avoided unless they must be used with software that requires them to be accessed from disk.

WorkspaceDbs are stored in access-controlled WorkspaceContainers backed by cloud storage. So, the structure of a Workspace is a hierarchy: a Workspace contains any number of WorkspaceContainers, each of which contains any number of WorkspaceDbs, each of which contains any number of resources. The container is the unit of access control - anyone who has read access to the container can read the contents of any WorkspaceDb inside it, and anyone with write access to the container can modify its contents.

Creating workspace resources

Note: Creating and managing data in workspaces is a task for administrators, not end-users. Administrators will typically use a specialized application with a user interface designed for this task. For the purposes of illustration, the following examples will use the WorkspaceEditor API directly.

LandscapePro allows users to decorate a landscape with a variety of trees and other flora. So, trees are one of the kinds of resources the application needs to access to perform its functions. Naturally, they should be stored in the Workspace. Let's create a WorkspaceDb to hold trees of the genus Cornus.

Since every WorkspaceDb must reside inside a WorkspaceContainer, we must first create a container. Creating a container also creates a default WorkspaceDb. In the createTreeDb function below, we will set up the container's default WorkspaceDb to be an as-yet empty tree database.

const editor = WorkspaceEditor.construct(); async function createTreeDb(genus: string): Promise<EditableWorkspaceDb> { const label = `Trees ${genus}`; const description = `Trees of the genus ${genus}`; const container: EditableWorkspaceContainer = await editor.createNewCloudContainer({ // A description of the new `CloudSQLite.Container` for use as a `Workspace` container. metadata: { label: `Workspace for {label}`, description, }, // Ownership and datacenter are defined by the iTwin. Access rights are granted by RBAC administrators of the iTwin. scope: { iTwinId }, // The manifest to be embedded inside the default WorkspaceDb. manifest: { // A user-facing name for the WorkspaceDb. workspaceName: label, // A description of the WorkspaceDb's contents and purpose. description, // The name of someone (typically an administrator) who can provide help and information // about this WorkspaceDb. contactName: "Lief E. Greene", }, }); container.acquireWriteLock("Lief E. Greene"); return container.getEditableDb({}); }

Now, let's define what a "tree" resource looks like, and add some of them to a new WorkspaceDb. To do so, we'll need to make a new version of the empty "cornus" WorkspaceDb we created above. WorkspaceDbs use semantic versioning, starting with a pre-release version (0.0.0). Each version of a given WorkspaceDb becomes immutable once published to cloud storage, with the exception of pre-release versions. The process for creating a new version of a WorkspaceDb is as follows:

  1. Acquire the container's write lock. Only one person - the current holder of the lock - can make changes to the contents of a given container at any given time.
  2. Create a new version of an existing WorkspaceDb.
  3. Open the new version of the db for writing.
  4. Modify the contents of the db.
  5. Close the db.
  6. (Optionally, create more new versions of WorkspaceDbs in the same container).
  7. Release the container's write lock.

Once the write lock is released, the new versions of the WorkspaceDbs are published to cloud storage and become immutable. Alternatively, you can discard all of your changes via EditableWorkspaceContainer.abandonChanges - this also releases the write lock.

Semantic versioning and immutability of published versions are core features of Workspaces. Newly created WorkspaceDbs start with a pre-release version that bypasses these features. Therefore, after creating a WorkspaceDb, administrators should load it with the desired resources and then publish version 1.0.0. Pre-release versions are useful when making work-in-progress adjustments or sharing changes prior to publishing a new version.

interface TreeResource { commonName: string; hardiness: HardinessRange; light: "full" | "shade" | "partial"; } function addTree(treeDb: EditableWorkspaceDb, species: string, tree: TreeResource): void { // We use a prefix to distinguish trees from other kinds of resources that might be present in the same WorkspaceDb. const resourceName = `landscapePro/tree/${species}`; treeDb.addString(resourceName, JSON.stringify(tree)); } let cornusDb = await createTreeDb("cornus"); cornusDb.open(); addTree(cornusDb, "alternifolia", { commonName: "Pagoda Dogwood", hardiness: { minimum: 4, maximum: 8 }, light: "full", }); addTree(cornusDb, "asperifolia", { commonName: "Roughleaf Dogwood", hardiness: { minimum: 9, maximum: 9 }, light: "full", }); // Close the db and release the write lock, which publishes the changes to the cloud and makes // them visible to other users. cornusDb.close(); cornusDb.container.releaseWriteLock(); // We have just created and populated a prerelease version (0.0.0) of the cornusDb. // Let's mint version 1.0.0. // As before, the write lock must be acquired and released, and the db must be opened and closed // to publish the changes to the cloud. cornusDb.container.acquireWriteLock("Lief E. Greene"); const cornusMajorProps = (await cornusDb.container.createNewWorkspaceDbVersion({ versionType: "major", })).newDb; cornusDb = cornusDb.container.getEditableDb(cornusMajorProps); cornusDb.open(); cornusDb.close(); cornusDb.container.releaseWriteLock();

In the example above, we created version 1.1.0 of the "cornus" WorkspaceDb, added two species of dogwood tree to it, and uploaded it. Later, we might create a patched version 1.1.1 that includes a species of dogwood that we forgot in version 1.1.0, and add a second WorkspaceDb to hold trees of the genus abies:

cornusDb.container.acquireWriteLock("Lief E. Greene"); const cornusPatchProps = (await cornusDb.container.createNewWorkspaceDbVersion({ versionType: "patch", })).newDb; cornusDb = cornusDb.container.getEditableDb(cornusPatchProps); cornusDb.open(); addTree(cornusDb, "racemosa", { commonName: "Northern Swamp Dogwood", hardiness: { minimum: 4, maximum: 9 }, light: "full", }); cornusDb.close(); cornusDb.container.releaseWriteLock(); let abiesDb = await createTreeDb("abies"); abiesDb.open(); addTree(abiesDb, "amabilis", { commonName: "Pacific Silver Fir", hardiness: { minimum: 5, maximum: 5 }, light: "full", }); addTree(abiesDb, "balsamea", { commonName: "Balsam Fir", hardiness: { minimum: 3, maximum: 6 }, light: "full", }); abiesDb.close(); abiesDb.container.releaseWriteLock(); // Mint 1.0.0 of abiesDb abiesDb.container.acquireWriteLock("Lief E. Greene"); const abiesMajorProps = (await abiesDb.container.createNewWorkspaceDbVersion({ versionType: "major", })).newDb; abiesDb = abiesDb.container.getEditableDb(abiesMajorProps); abiesDb.open(); abiesDb.close(); abiesDb.container.releaseWriteLock();

Note that we created one WorkspaceContainer to hold versions of the "cornus" WorkspaceDb, and a separate container for the "abies" WorkspaceDb. Alternatively, we could have put both WorkspaceDbs into the same container. However, because access control is enforced at the container level, maintaining a 1:1 mapping between containers and WorkspaceDbs simplifies things and reduces contention for the container's write lock.

Accessing workspace resources

Now that we have some WorkspaceDbs, we can configure our Settings to use them. The LandscapePro schema defines a "landscapePro/flora/treeDbs" setting that extends the type itwin/core/workspace/workspaceDbList. This type defines an array of WorkspaceDbProps, and overrides the combineArray property to true.

In the iTwin-scoped workflow, administrators persist this setting through IModelHost.saveSettingDictionary so every iModel in the iTwin resolves the same tree databases:

assert(undefined !== cornusDb.cloudProps); assert(undefined !== abiesDb.cloudProps); await IModelHost.saveSettingDictionary(iTwinId, "landscapePro/flora", { "landscapePro/flora/treeDbs": [ { ...cornusDb.cloudProps }, { ...abiesDb.cloudProps }, ], }); const workspaceForTreeDbs = await IModelHost.getITwinWorkspace(iTwinId); const workspaceTreeDbs = await workspaceForTreeDbs.getWorkspaceDbs({ settingName: "landscapePro/flora/treeDbs" });

With that setting in place, let's write a function that queries the resolved tree databases and returns every tree that can survive in a specified USDA hardiness zone:

async function getAvailableTrees(hardiness: HardinessRange): Promise<TreeResource[]> { // Resolve the list of WorkspaceDbs from the setting. const dbs = await iModel.workspace.getWorkspaceDbs({ settingName: "landscapePro/flora/treeDbs" }); // Query for all the trees in all the WorkspaceDbs and collect a list of those that match the hardiness criterion. const trees: TreeResource[] = []; Workspace.queryResources({ dbs, // Include only tree resources, as indicated by their name prefix. namePattern: "landscapePro/tree/%", nameCompare: "LIKE", callback: (resources: Iterable<{ name: string, db: WorkspaceDb }>) => { for (const resource of resources) { // Look up the tree as stringified JSON in the current WorkspaceDb. const str = resource.db.getString(resource.name); assert(undefined !== str); const tree = JSON.parse(str) as TreeResource; if (tree.hardiness.minimum <= hardiness.maximum && hardiness.minimum <= tree.hardiness.maximum) { trees.push(tree); } } }, }); return trees; }

Because the setting is stored at the iTwin scope, every iModel in the iTwin resolves the same tree resource list. When you publish a new version of a tree WorkspaceDb, update the iTwin setting so all iModels pick up the change:

await IModelHost.saveSettingDictionary(iTwinId, "landscapePro/flora", { "landscapePro/flora/treeDbs": [ { ...cornusDb.cloudProps, version: "1.1.1" }, { ...abiesDb.cloudProps }, ], });

In this example, the setting explicitly references version 1.1.1 of the cornus WorkspaceDb — the patch that added the Northern Swamp Dogwood. If we had omitted the WorkspaceDbProps.version property, it would have defaulted to the latest available version. In this case the result would be the same (1.1.1), but in the future, if a newer version were published, it would be picked up automatically. When you need deterministic, reproducible behavior — for example, in a regulated workflow — set version to a specific value to pin it. We could also configure the version more precisely using semantic versioning rules to specify a range of acceptable versions. When compatible new versions of a WorkspaceDb are published, the workspace would automatically consume them without requiring any explicit changes to its Settings.

Last Updated: 08 April, 2026