Repository: iTwin/itwinjs-core
Pull request #7823: Improve replacements for deprecated schema metadata methods in backend
Author: login: RohitPtnkr1996
Created: 2025-03-11T09:22:34Z | Merged: 2025-03-14T16:51:34Z

Fixes https://github.com/iTwin/itwinjs-backlog/issues/1424


Diff:
diff --git a/common/api/core-backend.api.md b/common/api/core-backend.api.md
index 059d84b5d577..85b91976f35f 100644
--- a/common/api/core-backend.api.md
+++ b/common/api/core-backend.api.md
@@ -184,6 +184,7 @@ import { Polyface } from '@itwin/core-geometry';
 import { PolyfaceData } from '@itwin/core-geometry';
 import { PolyfaceVisitor } from '@itwin/core-geometry';
 import { PropertyCallback } from '@itwin/core-common';
+import { PropertyHandler } from '@itwin/ecschema-metadata';
 import { QueryBinder } from '@itwin/core-common';
 import { QueryOptions } from '@itwin/core-common';
 import { QueryRowFormat } from '@itwin/core-common';
@@ -2351,6 +2352,7 @@ export class Entity {
     protected collectReferenceConcreteIds: (_referenceIds: EntityReferenceSet) => void;
     // @beta
     protected collectReferenceIds(_referenceIds: EntityReferenceSet): void;
+    executeForEachProperty(func: PropertyHandler, includeCustom?: boolean): Promise<void>;
     // @deprecated
     forEachProperty(func: PropertyCallback, includeCustom?: boolean): void;
     // @beta
@@ -3231,6 +3233,7 @@ export abstract class IModelDb extends IModel {
     static forEachMetaData(iModel: IModelDb, classFullName: string, wantSuper: boolean, func: PropertyCallback, includeCustom?: boolean): void;
     // @deprecated
     forEachMetaData(classFullName: string, wantSuper: boolean, func: PropertyCallback, includeCustom?: boolean): void;
+    static forEachProperty(iModel: IModelDb, classFullName: string, wantSuper: boolean, func: PropertyHandler, includeCustom?: boolean): Promise<void>;
     generateElementGraphics(request: ElementGraphicsRequestProps): Promise<Uint8Array | undefined>;
     getBriefcaseId(): BriefcaseId;
     getGeoCoordinatesFromIModelCoordinates(props: GeoCoordinatesRequestProps): Promise<GeoCoordinatesResponseProps>;
diff --git a/common/api/core-common.api.md b/common/api/core-common.api.md
index 5450a6f318ab..9b880f33ac01 100644
--- a/common/api/core-common.api.md
+++ b/common/api/core-common.api.md
@@ -7090,7 +7090,7 @@ export interface ProjectionProps {
     zoneNumber?: number;
 }
 
-// @beta
+// @beta @deprecated
 export type PropertyCallback = (name: string, meta: PropertyMetaData) => void;
 
 // @beta @deprecated
diff --git a/common/api/ecschema-metadata.api.md b/common/api/ecschema-metadata.api.md
index e6e40cefd334..471a4ce6b2c7 100644
--- a/common/api/ecschema-metadata.api.md
+++ b/common/api/ecschema-metadata.api.md
@@ -796,16 +796,6 @@ export interface InvertedUnitProps extends SchemaItemProps {
     readonly unitSystem: string;
 }
 
-// @beta
-export interface ISchemaItemLocater {
-    getSchemaItem(schemaItemKey: SchemaItemKey): Promise<SchemaItem | undefined>;
-    // (undocumented)
-    getSchemaItem<T extends typeof SchemaItem>(schemaItemKey: SchemaItemKey, itemConstructor: T): Promise<InstanceType<T> | undefined>;
-    getSchemaItemSync(schemaItemKey: SchemaItemKey): SchemaItem | undefined;
-    // (undocumented)
-    getSchemaItemSync<T extends typeof SchemaItem>(schemaItemKey: SchemaItemKey, itemConstructor: T): InstanceType<T> | undefined;
-}
-
 // @beta
 export interface ISchemaLocater {
     getSchema(schemaKey: SchemaKey, matchType: SchemaMatchType, context: SchemaContext): Promise<Schema | undefined>;
@@ -1078,7 +1068,7 @@ export class OverrideFormat {
     // (undocumented)
     get type(): FormatType;
     // (undocumented)
-    get units(): [Unit | InvertedUnit, string | undefined][] | undefined;
+    get units(): [InvertedUnit | Unit, string | undefined][] | undefined;
     // (undocumented)
     get uomSeparator(): string;
 }
@@ -1377,6 +1367,9 @@ export interface PropertyCategoryProps extends SchemaItemProps {
     readonly priority: number;
 }
 
+// @beta
+export type PropertyHandler = (name: string, property: Property) => void;
+
 // @beta (undocumented)
 export interface PropertyProps {
     // (undocumented)
@@ -1830,7 +1823,7 @@ export class SchemaCache implements ISchemaLocater {
 }
 
 // @beta
-export class SchemaContext implements ISchemaItemLocater {
+export class SchemaContext {
     constructor();
     addFallbackLocater(locater: ISchemaLocater): void;
     addLocater(locater: ISchemaLocater): void;
@@ -1839,6 +1832,8 @@ export class SchemaContext implements ISchemaItemLocater {
     addSchemaItem(schemaItem: SchemaItem): Promise<void>;
     addSchemaPromise(schemaInfo: SchemaInfo, schema: Schema, schemaPromise: Promise<Schema>): Promise<void>;
     addSchemaSync(schema: Schema): void;
+    // (undocumented)
+    forEachProperty<T extends typeof SchemaItem>(classFullName: string, wantSuper: boolean, func: PropertyHandler, itemConstructor: T, includeCustom?: boolean): Promise<void>;
     // @internal
     getCachedSchema(schemaKey: SchemaKey, matchType?: SchemaMatchType): Promise<Schema | undefined>;
     // @internal
@@ -1846,17 +1841,26 @@ export class SchemaContext implements ISchemaItemLocater {
     getKnownSchemas(): Schema[];
     getSchema(schemaKey: SchemaKey, matchType?: SchemaMatchType): Promise<Schema | undefined>;
     getSchemaInfo(schemaKey: SchemaKey, matchType: SchemaMatchType): Promise<SchemaInfo | undefined>;
-    getSchemaItem(schemaItemKey: SchemaItemKey): Promise<SchemaItem | undefined>;
+    getSchemaItem<T extends typeof SchemaItem>(schemaNameOrKey: SchemaItemKey | string, itemNameOrCtor?: T): Promise<InstanceType<T> | undefined>;
+    // (undocumented)
+    getSchemaItem<T extends typeof SchemaItem>(schemaNameOrKey: SchemaItemKey | string, itemNameOrCtor?: T): Promise<SchemaItem | undefined>;
     // (undocumented)
-    getSchemaItem<T extends typeof SchemaItem>(schemaItemKey: SchemaItemKey, itemConstructor: T): Promise<InstanceType<T> | undefined>;
+    getSchemaItem<T extends typeof SchemaItem>(schemaNameOrKey: string, itemNameOrCtor: string, itemConstructor?: T): Promise<InstanceType<T> | undefined>;
+    // (undocumented)
+    getSchemaItem<T extends typeof SchemaItem>(schemaNameOrKey: string, itemNameOrCtor: string, itemConstructor?: T): Promise<SchemaItem | undefined>;
     getSchemaItems(): IterableIterator<SchemaItem>;
-    getSchemaItemSync(schemaItemKey: SchemaItemKey): SchemaItem | undefined;
+    getSchemaItemSync<T extends typeof SchemaItem>(schemaNameOrKey: SchemaItemKey | string, itemNameOrCtor?: T): InstanceType<T> | undefined;
+    // (undocumented)
+    getSchemaItemSync<T extends typeof SchemaItem>(schemaNameOrKey: SchemaItemKey | string, itemNameOrCtor?: T): SchemaItem | undefined;
+    // (undocumented)
+    getSchemaItemSync<T extends typeof SchemaItem>(schemaNameOrKey: string, itemNameOrCtor: string, itemConstructor?: T): InstanceType<T> | undefined;
     // (undocumented)
-    getSchemaItemSync<T extends typeof SchemaItem>(schemaItemKey: SchemaItemKey, itemConstructor: T): InstanceType<T> | undefined;
+    getSchemaItemSync<T extends typeof SchemaItem>(schemaNameOrKey: string, itemNameOrCtor: string, itemConstructor?: T): SchemaItem | undefined;
     getSchemaSync(schemaKey: SchemaKey, matchType?: SchemaMatchType): Schema | undefined;
     // (undocumented)
     get locaters(): ISchemaLocater[];
     schemaExists(schemaKey: SchemaKey): boolean;
+    tryGetSchemaItem<T extends typeof SchemaItem>(classFullName: string, itemConstructor: T): InstanceType<T> | undefined;
 }
 
 // @internal
diff --git a/common/api/summary/core-common.exports.csv b/common/api/summary/core-common.exports.csv
index f2b874fccdcf..3439a3136149 100644
--- a/common/api/summary/core-common.exports.csv
+++ b/common/api/summary/core-common.exports.csv
@@ -638,6 +638,7 @@ public;class;Projection
 public;type;ProjectionMethod
 public;interface;ProjectionProps
 beta;type;PropertyCallback
+deprecated;type;PropertyCallback
 beta;class;PropertyMetaData
 deprecated;class;PropertyMetaData
 public;class;PropertyMetaDataMap
diff --git a/common/api/summary/ecschema-metadata.exports.csv b/common/api/summary/ecschema-metadata.exports.csv
index 59e68ba78e3c..980995f2c2c7 100644
--- a/common/api/summary/ecschema-metadata.exports.csv
+++ b/common/api/summary/ecschema-metadata.exports.csv
@@ -51,7 +51,6 @@ internal;function;getFormatProps
 beta;interface;HasMixins
 beta;class;InvertedUnit
 beta;interface;InvertedUnitProps
-beta;interface;ISchemaItemLocater
 beta;interface;ISchemaLocater
 beta;interface;ISchemaPartVisitor
 beta;function;isSupportedSchemaItemType
@@ -102,6 +101,7 @@ beta;function;primitiveTypeToString
 beta;class;Property
 beta;class;PropertyCategory
 beta;interface;PropertyCategoryProps
+beta;type;PropertyHandler
 beta;interface;PropertyProps
 beta;enum;PropertyType
 beta;function;propertyTypeToString
diff --git a/common/changes/@itwin/core-backend/rohitptnkr-replace-deprecated-metadata-functions_2025-03-13-12-59.json b/common/changes/@itwin/core-backend/rohitptnkr-replace-deprecated-metadata-functions_2025-03-13-12-59.json
new file mode 100644
index 000000000000..460142f82377
--- /dev/null
+++ b/common/changes/@itwin/core-backend/rohitptnkr-replace-deprecated-metadata-functions_2025-03-13-12-59.json
@@ -0,0 +1,10 @@
+{
+  "changes": [
+    {
+      "packageName": "@itwin/core-backend",
+      "comment": "Updated comments to mention new replacements for the deprecated methods forEachProperty, getMetaData, tryGetMetaData and forEachMetaData.",
+      "type": "none"
+    }
+  ],
+  "packageName": "@itwin/core-backend"
+}
\ No newline at end of file
diff --git a/common/changes/@itwin/core-common/rohitptnkr-replace-deprecated-metadata-functions_2025-03-13-12-59.json b/common/changes/@itwin/core-common/rohitptnkr-replace-deprecated-metadata-functions_2025-03-13-12-59.json
new file mode 100644
index 000000000000..d1ac065f5d72
--- /dev/null
+++ b/common/changes/@itwin/core-common/rohitptnkr-replace-deprecated-metadata-functions_2025-03-13-12-59.json
@@ -0,0 +1,10 @@
+{
+  "changes": [
+    {
+      "packageName": "@itwin/core-common",
+      "comment": "",
+      "type": "none"
+    }
+  ],
+  "packageName": "@itwin/core-common"
+}
\ No newline at end of file
diff --git a/common/changes/@itwin/ecschema-editing/rohitptnkr-replace-deprecated-metadata-functions_2025-03-13-12-59.json b/common/changes/@itwin/ecschema-editing/rohitptnkr-replace-deprecated-metadata-functions_2025-03-13-12-59.json
new file mode 100644
index 000000000000..b85e40a5600b
--- /dev/null
+++ b/common/changes/@itwin/ecschema-editing/rohitptnkr-replace-deprecated-metadata-functions_2025-03-13-12-59.json
@@ -0,0 +1,10 @@
+{
+  "changes": [
+    {
+      "packageName": "@itwin/ecschema-editing",
+      "comment": "Updated getSchemaItem methods from MergingSchemaContext to reflect changes made to methods from the base class SchemaContext.",
+      "type": "none"
+    }
+  ],
+  "packageName": "@itwin/ecschema-editing"
+}
\ No newline at end of file
diff --git a/common/changes/@itwin/ecschema-metadata/rohitptnkr-replace-deprecated-metadata-functions_2025-03-13-12-59.json b/common/changes/@itwin/ecschema-metadata/rohitptnkr-replace-deprecated-metadata-functions_2025-03-13-12-59.json
new file mode 100644
index 000000000000..c683fecbf8fa
--- /dev/null
+++ b/common/changes/@itwin/ecschema-metadata/rohitptnkr-replace-deprecated-metadata-functions_2025-03-13-12-59.json
@@ -0,0 +1,10 @@
+{
+  "changes": [
+    {
+      "packageName": "@itwin/ecschema-metadata",
+      "comment": "Added replacement methods for the now deprecated methods forEachProperty, getMetaData, tryGetMetaData and forEachMetaData.",
+      "type": "none"
+    }
+  ],
+  "packageName": "@itwin/ecschema-metadata"
+}
\ No newline at end of file
diff --git a/common/config/azure-pipelines/jobs/fast-ci.yaml b/common/config/azure-pipelines/jobs/fast-ci.yaml
index 585d6507d5f6..88b4691bfa0f 100644
--- a/common/config/azure-pipelines/jobs/fast-ci.yaml
+++ b/common/config/azure-pipelines/jobs/fast-ci.yaml
@@ -24,7 +24,7 @@ pr:
   drafts: false
   branches:
     include:
-      - master
+      - rob/deprecate-metadata
       - release/*
   paths:
     exclude:
diff --git a/core/backend/src/Entity.ts b/core/backend/src/Entity.ts
index a31023fa6481..86880f2e37fc 100644
--- a/core/backend/src/Entity.ts
+++ b/core/backend/src/Entity.ts
@@ -10,7 +10,7 @@ import { Id64, Id64String } from "@itwin/core-bentley";
 import { EntityProps, EntityReferenceSet, PropertyCallback, PropertyMetaData } from "@itwin/core-common";
 import type { IModelDb } from "./IModelDb";
 import { Schema } from "./Schema";
-import { EntityClass, SchemaItemKey } from "@itwin/ecschema-metadata";
+import { EntityClass, PropertyHandler, SchemaItemKey } from "@itwin/ecschema-metadata";
 
 /** Represents one of the fundamental building block in an [[IModelDb]]: as an [[Element]], [[Model]], or [[Relationship]].
  * Every subclass of Entity represents one BIS [ECClass]($ecschema-metadata).
@@ -95,13 +95,38 @@ export class Entity {
    * @param func The callback to be invoked on each property
    * @param includeCustom If true (default), include custom-handled properties in the iteration. Otherwise, skip custom-handled properties.
    * @note Custom-handled properties are core properties that have behavior enforced by C++ handlers.
-   * @deprecated in 5.0. Use [[getMetaData]] to get the metadata and iterate over the properties
+   * @deprecated in 5.0. Use [[executeForEachProperty]] to get the metadata and iterate over the properties
    */
+  // eslint-disable-next-line @typescript-eslint/no-deprecated
   public forEachProperty(func: PropertyCallback, includeCustom: boolean = true) {
     // eslint-disable-next-line @typescript-eslint/no-deprecated
     this.iModel.forEachMetaData(this.classFullName, true, func, includeCustom);
   }
 
+  /**
+   * Invoke a callback on each property of the specified class, optionally including superclass properties.
+   *
+   * This method iterates over the properties of the class, including those inherited from base classes and mixins,
+   * and applies the provided callback function to each property. The callback function can be used to perform
+   * operations on each property, such as logging or processing property metadata.
+   *
+   * @param func The callback to be invoked on each property. The callback receives the property name and the property metadata as arguments.
+   * @param includeCustom If true (default), include custom-handled properties in the iteration. Otherwise, skip custom-handled properties.
+   *
+   * @example
+   * ```typescript
+   * const callback: PropertyHandler = (name, property) => {
+   *   console.log(`Property Name: ${name}`);
+   *   console.log(`Property Class:`, property.class);
+   * };
+   *
+   * await entity.executeForEachProperty(callback);
+   * ```
+   */
+  public async executeForEachProperty(func: PropertyHandler, includeCustom: boolean = true) {
+    await this.iModel.schemaContext.forEachProperty(this.classFullName, true, func, EntityClass, includeCustom);
+  }
+
   /** Get the full BIS class name of this Entity in the form "schema:class" */
   public static get classFullName(): string { return `${this.schema.schemaName}:${this.className}`; }
 
diff --git a/core/backend/src/IModelDb.ts b/core/backend/src/IModelDb.ts
index 4614c7ff490c..4939a9d5d1d4 100644
--- a/core/backend/src/IModelDb.ts
+++ b/core/backend/src/IModelDb.ts
@@ -70,7 +70,7 @@ import { createNoOpLockControl } from "./internal/NoLocks";
 import { IModelDbFonts } from "./IModelDbFonts";
 import { createIModelDbFonts } from "./internal/IModelDbFontsImpl";
 import { _close, _hubAccess, _nativeDb, _releaseAllLocks } from "./internal/Symbols";
-import { SchemaContext, SchemaJsonLocater } from "@itwin/ecschema-metadata";
+import { EntityClass, PropertyHandler, SchemaContext, SchemaJsonLocater } from "@itwin/ecschema-metadata";
 import { SchemaMap } from "./Schema";
 
 // spell:ignore fontid fontmap
@@ -1210,7 +1210,7 @@ export abstract class IModelDb extends IModel {
 
   /** Get metadata for a class. This method will load the metadata from the iModel into the cache as a side-effect, if necessary.
    * @throws [[IModelError]] if the metadata cannot be found nor loaded.
-   * @deprecated in 5.0. Use [schemaContext] instead.
+   * @deprecated in 5.0. Use [[getSchemaItem]] from SchemaContext class instead.
    */
   // eslint-disable-next-line @typescript-eslint/no-deprecated
   public getMetaData(classFullName: string): EntityMetaData {
@@ -1228,7 +1228,7 @@ export abstract class IModelDb extends IModel {
   }
 
   /** Identical to [[getMetaData]], except it returns `undefined` instead of throwing an error if the metadata cannot be found nor loaded.
-   * @deprecated in 5.0. Use [schemaContext] instead.
+   * @deprecated in 5.0. Use [[tryGetSchemaItem]] from SchemaContext class instead.
    */
   // eslint-disable-next-line @typescript-eslint/no-deprecated
   public tryGetMetaData(classFullName: string): EntityMetaData | undefined {
@@ -1247,21 +1247,46 @@ export abstract class IModelDb extends IModel {
    * @param func The callback to be invoked on each property
    * @param includeCustom If true (default), include custom-handled properties in the iteration. Otherwise, skip custom-handled properties.
    * @note Custom-handled properties are core properties that have behavior enforced by C++ handlers.
-   * @deprecated in 5.0. Use [schemaContext] instead.
+   * @deprecated in 5.0. Use [[forEachProperty]] instead.
    */
+  // eslint-disable-next-line @typescript-eslint/no-deprecated
   public static forEachMetaData(iModel: IModelDb, classFullName: string, wantSuper: boolean, func: PropertyCallback, includeCustom: boolean = true) {
     // eslint-disable-next-line @typescript-eslint/no-deprecated
     iModel.forEachMetaData(classFullName, wantSuper, func, includeCustom);
   }
 
+  /**
+   * Invoke a callback on each property of the specified class, optionally including superclass properties.
+   *
+   * @param iModel The IModelDb instance that contains the schema.
+   * @param classFullName The full class name to load the metadata, if necessary.
+   * @param wantSuper If true, superclass properties will also be processed.
+   * @param func The callback to be invoked on each property.
+   * @param includeCustom If true (default), include custom-handled properties in the iteration. Otherwise, skip custom-handled properties.
+   *
+   * @example
+   * ```typescript
+   * const callback: PropertyHandler = (name, property) => {
+   *   console.log(`Property Name: ${name}`);
+   *   console.log(`Property Class:`, property.class);
+   * };
+   *
+   * IModelDb.forEachProperty(imodel, "BisCore:Element", true, callback);
+   * ```
+   */
+  public static async forEachProperty(iModel: IModelDb, classFullName: string, wantSuper: boolean, func: PropertyHandler, includeCustom: boolean = true): Promise<void> {
+    await iModel.schemaContext.forEachProperty(classFullName, wantSuper, func, EntityClass, includeCustom);
+  }
+
   /** Invoke a callback on each property of the specified class, optionally including superclass properties.
    * @param classFullName The full class name to load the metadata, if necessary
    * @param wantSuper If true, superclass properties will also be processed
    * @param func The callback to be invoked on each property
    * @param includeCustom If true (default), include custom-handled properties in the iteration. Otherwise, skip custom-handled properties.
    * @note Custom-handled properties are core properties that have behavior enforced by C++ handlers.
-   * @deprecated in 5.0. Use [schemaContext] instead.
+   * @deprecated in 5.0. Use [forEachProperty] from SchemaContext class instead.
    */
+  // eslint-disable-next-line @typescript-eslint/no-deprecated
   public forEachMetaData(classFullName: string, wantSuper: boolean, func: PropertyCallback, includeCustom: boolean = true) {
     // eslint-disable-next-line @typescript-eslint/no-deprecated
     const meta = this.getMetaData(classFullName); // will load if necessary
diff --git a/core/backend/src/test/element/ElementRoundTrip.test.ts b/core/backend/src/test/element/ElementRoundTrip.test.ts
index ce0129534b58..e1013bc4f214 100644
--- a/core/backend/src/test/element/ElementRoundTrip.test.ts
+++ b/core/backend/src/test/element/ElementRoundTrip.test.ts
@@ -12,6 +12,7 @@ import { Angle, Arc3d, Cone, IModelJson as GeomJson, LineSegment3d, Point2d, Poi
 import { _nativeDb, ECSqlStatement, IModelDb, IModelJsFs, PhysicalModel, PhysicalObject, SnapshotDb, SpatialCategory } from "../../core-backend";
 import { ElementRefersToElements } from "../../Relationship";
 import { IModelTestUtils } from "../IModelTestUtils";
+import { EntityClass, RelationshipClass } from "@itwin/ecschema-metadata";
 
 /* eslint-disable @typescript-eslint/naming-convention */
 
@@ -565,9 +566,6 @@ describe("Element and ElementAspect roundtrip test for all type of properties",
       }
     } as unknown as TestElement;
 
-    // eslint-disable-next-line @typescript-eslint/no-deprecated
-    const relClassId = imodel.getMetaData("BisCore:ModelContainsElements").classId;
-
     // verify inserted element properties
     const actualValue = imodel.elements.getElementProps<TestElement>(id);
     verifyTestElement(actualValue, expectedValue);
@@ -593,14 +591,18 @@ describe("Element and ElementAspect roundtrip test for all type of properties",
       verifySystemProperty(stmt.getRow() as TestElement, expectedSystemProperty);
     });
 
+    const testElementMetaData = imodel.schemaContext.getSchemaItemSync("ElementRoundTripTest", "TestElement", EntityClass);
+    assert.isDefined(testElementMetaData);
+    const relClassMetaData = imodel.schemaContext.getSchemaItemSync("BisCore", "ModelContainsElements", RelationshipClass);
+    assert.isDefined(relClassMetaData);
+
     // Verify system properties via concurrent query
-    let reader = imodel.createQueryReader("SELECT ECInstanceId, ECClassId, Model.Id, Model.RelECClassId FROM ts.TestElement", undefined, { rowFormat: QueryRowFormat.UseECSqlPropertyNames });
+    let reader = imodel.createQueryReader("SELECT ECInstanceId, ec_classname(ECClassId) as className, Model.Id, ec_classname(Model.RelECClassId) as relClassName FROM ts.TestElement", undefined, { rowFormat: QueryRowFormat.UseECSqlPropertyNames });
     assert.isTrue(await reader.step());
     assert.equal(reader.current.ECInstanceId, id);
-    // eslint-disable-next-line @typescript-eslint/no-deprecated
-    assert.equal(reader.current.ECClassId, geomElement.getClassMetaData()?.classId);
+    assert.equal(reader.current.className.replace(":", "."), testElementMetaData?.fullName);
     assert.equal(reader.current.Id, newModelId);
-    assert.equal(reader.current.RelECClassId, relClassId);
+    assert.equal(reader.current.relClassName.replace(":", "."), relClassMetaData?.fullName);
     assert.isFalse(await reader.step());
 
     // update the element autohandled properties
@@ -641,13 +643,12 @@ describe("Element and ElementAspect roundtrip test for all type of properties",
     });
 
     // Verify system properties via concurrent query
-    reader = imodel.createQueryReader("SELECT ECInstanceId, ECClassId, Model.Id, Model.RelECClassId FROM ts.TestElement", undefined, { rowFormat: QueryRowFormat.UseECSqlPropertyNames });
+    reader = imodel.createQueryReader("SELECT ECInstanceId, ec_classname(ECClassId) as className, Model.Id, ec_classname(Model.RelECClassId) as relClassName FROM ts.TestElement", undefined, { rowFormat: QueryRowFormat.UseECSqlPropertyNames });
     assert.isTrue(await reader.step());
     assert.equal(reader.current.ECInstanceId, id);
-    // eslint-disable-next-line @typescript-eslint/no-deprecated
-    assert.equal(reader.current.ECClassId, geomElement.getClassMetaData()?.classId);
+    assert.equal(reader.current.className.replace(":", "."), testElementMetaData?.fullName);
     assert.equal(reader.current.Id, newModelId);
-    assert.equal(reader.current.RelECClassId, relClassId);
+    assert.equal(reader.current.relClassName.replace(":", "."), relClassMetaData?.fullName);
     assert.isFalse(await reader.step());
 
     imodel.close();
@@ -683,15 +684,19 @@ describe("Element and ElementAspect roundtrip test for all type of properties",
       },
     } as unknown as ElementAspectProps;
 
+    const aspectMetaData = await iModel.schemaContext.getSchemaItem("ElementRoundTripTest.TestElementAspect", EntityClass);
+    assert.isDefined(aspectMetaData);
+
+    const relMetaData = await iModel.schemaContext.getSchemaItem("BisCore.ElementOwnsUniqueAspect", RelationshipClass);
+    assert.isDefined(relMetaData);
+
     // Verify via a concurrent query
-    const reader = iModel.createQueryReader("SELECT ECInstanceId, ECClassId, Element.Id, Element.RelECClassId FROM ts.TestElementAspect", undefined, { rowFormat: QueryRowFormat.UseECSqlPropertyNames });
+    const reader = iModel.createQueryReader("SELECT ECInstanceId, ec_classname(ECClassId) as className, Element.Id, ec_classname(Element.RelECClassId) as relClassName FROM ts.TestElementAspect", undefined, { rowFormat: QueryRowFormat.UseECSqlPropertyNames });
     assert.isTrue(await reader.step());
     assert.equal(reader.current.ECInstanceId, elementAspectId);
-    // eslint-disable-next-line @typescript-eslint/no-deprecated
-    assert.equal(reader.current.ECClassId, iModel.getMetaData("ElementRoundTripTest:TestElementAspect").classId);
+    assert.equal(reader.current.className.replace(":", "."), aspectMetaData?.fullName);
     assert.equal(reader.current.Id, elementId);
-    // eslint-disable-next-line @typescript-eslint/no-deprecated
-    assert.equal(reader.current.RelECClassId, iModel.getMetaData("BisCore:ElementOwnsUniqueAspect").classId);
+    assert.equal(reader.current.relClassName.replace(":", "."), relMetaData?.fullName);
     assert.isFalse(await reader.step());
 
     // Verify via an ECSql statement
@@ -816,20 +821,20 @@ describe("Element and ElementAspect roundtrip test for all type of properties",
       targetClassName: `ElementRoundTripTest.TestElement`,
     } as unknown as TestElementRefersToElements;
 
-    // eslint-disable-next-line @typescript-eslint/no-deprecated
-    const classId = imodel.getMetaData("ElementRoundTripTest:TestElementRefersToElements").classId;
-    // eslint-disable-next-line @typescript-eslint/no-deprecated
-    const testElementId = imodel.getMetaData("ElementRoundTripTest:TestElement").classId;
+    const classMetaData = await imodel.schemaContext.getSchemaItem("ElementRoundTripTest.TestElementRefersToElements", RelationshipClass);
+    assert.isDefined(classMetaData);
+    const elementMetaData = await imodel.schemaContext.getSchemaItem("ElementRoundTripTest.TestElement", EntityClass);
+    assert.isDefined(elementMetaData);
 
     // verify system properties via concurrent query
-    let reader = imodel.createQueryReader("SELECT ECInstanceId, ECClassId, SourceECInstanceId, SourceECClassId, TargetECInstanceid, TargetECClassId FROM ts.TestElementRefersToElements", undefined, { rowFormat: QueryRowFormat.UseECSqlPropertyNames });
+    let reader = imodel.createQueryReader("SELECT ECInstanceId, ec_classname(ECClassId) as className, SourceECInstanceId, ec_classname(SourceECClassId) as srcClassName, TargetECInstanceid, ec_classname(TargetECClassId) as trgtClassName FROM ts.TestElementRefersToElements", undefined, { rowFormat: QueryRowFormat.UseECSqlPropertyNames });
     assert.isTrue(await reader.step());
     assert.equal(reader.current.ECInstanceId, relationshipId);
-    assert.equal(reader.current.ECClassId, classId);
+    assert.equal(reader.current.className.replace(":", "."), classMetaData?.fullName);
     assert.equal(reader.current.SourceECInstanceId, elId1);
-    assert.equal(reader.current.SourceECClassId, testElementId);
+    assert.equal(reader.current.srcClassName.replace(":", "."), elementMetaData?.fullName);
     assert.equal(reader.current.TargetECInstanceid, elId2);
-    assert.equal(reader.current.TargetECClassId, testElementId);
+    assert.equal(reader.current.trgtClassName.replace(":", "."), elementMetaData?.fullName);
     assert.isFalse(await reader.step());
 
     // verify system properties via ecsql statement
@@ -871,14 +876,14 @@ describe("Element and ElementAspect roundtrip test for all type of properties",
     });
 
     // verify system properties via concurrent query
-    reader = imodel.createQueryReader("SELECT ECInstanceId, ECClassId, SourceECInstanceId, SourceECClassId, TargetECInstanceid, TargetECClassId FROM ts.TestElementRefersToElements", undefined, { rowFormat: QueryRowFormat.UseECSqlPropertyNames });
+    reader = imodel.createQueryReader("SELECT ECInstanceId, ec_classname(ECClassId) as className, SourceECInstanceId, ec_classname(SourceECClassId) as srcClassName, TargetECInstanceid, ec_classname(TargetECClassId) as trgtClassName FROM ts.TestElementRefersToElements", undefined, { rowFormat: QueryRowFormat.UseECSqlPropertyNames });
     assert.isTrue(await reader.step());
     assert.equal(reader.current.ECInstanceId, relationshipId);
-    assert.equal(reader.current.ECClassId, classId);
+    assert.equal(reader.current.className.replace(":", "."), classMetaData?.fullName);
     assert.equal(reader.current.SourceECInstanceId, elId1);
-    assert.equal(reader.current.SourceECClassId, testElementId);
+    assert.equal(reader.current.srcClassName.replace(":", "."), elementMetaData?.fullName);
     assert.equal(reader.current.TargetECInstanceid, elId2);
-    assert.equal(reader.current.TargetECClassId, testElementId);
+    assert.equal(reader.current.trgtClassName.replace(":", "."), elementMetaData?.fullName);
     assert.isFalse(await reader.step());
 
     // verify system properties via ecsql statement
diff --git a/core/backend/src/test/imodel/IModel.test.ts b/core/backend/src/test/imodel/IModel.test.ts
index ea5e138e2af4..d6538869d61a 100644
--- a/core/backend/src/test/imodel/IModel.test.ts
+++ b/core/backend/src/test/imodel/IModel.test.ts
@@ -9,10 +9,10 @@ import * as sinon from "sinon";
 import { DbResult, Guid, GuidString, Id64, Id64String, IModelStatus, Logger, OpenMode, ProcessDetector } from "@itwin/core-bentley";
 import {
   AxisAlignedBox3d, BisCodeSpec, BriefcaseIdValue, ChangesetIdWithIndex, Code, CodeScopeSpec, CodeSpec, ColorByName, ColorDef, DefinitionElementProps,
-  DisplayStyleProps, DisplayStyleSettings, DisplayStyleSettingsProps, EcefLocation, ElementProps, EntityMetaData, EntityProps, FilePropertyProps,
+  DisplayStyleProps, DisplayStyleSettings, DisplayStyleSettingsProps, EcefLocation, ElementProps, EntityProps, FilePropertyProps,
   FontMap, FontType, GeoCoordinatesRequestProps, GeoCoordStatus, GeographicCRS, GeographicCRSProps, GeometricElementProps, GeometryParams, GeometryStreamBuilder,
   ImageSourceFormat, IModel, IModelCoordinatesRequestProps, IModelError, LightLocationProps, MapImageryProps, PhysicalElementProps,
-  PointWithStatus, PrimitiveTypeCode, RelatedElement, RenderMode, SchemaState, SpatialViewDefinitionProps, SubCategoryAppearance, SubjectProps, TextureMapping,
+  PointWithStatus, RelatedElement, RenderMode, SchemaState, SpatialViewDefinitionProps, SubCategoryAppearance, SubjectProps, TextureMapping,
   TextureMapProps, TextureMapUnits, ViewDefinitionProps, ViewFlagProps, ViewFlags,
 } from "@itwin/core-common";
 import {
@@ -36,6 +36,7 @@ import { DisableNativeAssertions } from "../TestUtils";
 import { samplePngTexture } from "../imageData";
 import { performance } from "perf_hooks";
 import { _hubAccess } from "../../internal/Symbols";
+import { CustomAttributeClass, EntityClass, PrimitiveArrayProperty, PrimitiveOrEnumPropertyBase, PropertyType, propertyTypeToString, SchemaItemType } from "@itwin/ecschema-metadata";
 // spell-checker: disable
 
 async function getIModelError<T>(promise: Promise<T>): Promise<IModelError | undefined> {
@@ -1037,36 +1038,38 @@ describe("iModel", () => {
     assert.throws(() => imodel4.elements.getElement(childId2), IModelError);
   });
 
-  // eslint-disable-next-line @typescript-eslint/no-deprecated
-  function checkElementMetaData(obj: EntityMetaData) {
-    assert.isNotNull(obj);
-    assert.equal(obj.ecclass, Element.classFullName);
-    assert.isArray(obj.baseClasses);
-    assert.equal(obj.baseClasses.length, 0);
+  function checkElementMetaData(entityClass: EntityClass) {
+    assert.isNotNull(entityClass);
+    assert.equal(entityClass.fullName, Element.classFullName.replace(":", "."));
+    assert.isUndefined(entityClass.baseClass);
 
-    assert.isArray(obj.customAttributes);
     let foundClassHasHandler = false;
     let foundClassHasCurrentTimeStampProperty = false;
-    if (obj.customAttributes !== undefined) {
-      for (const ca of obj.customAttributes) {
-        if (ca.ecclass === "BisCore:ClassHasHandler")
-          foundClassHasHandler = true;
-        else if (ca.ecclass === "CoreCustomAttributes:ClassHasCurrentTimeStampProperty")
-          foundClassHasCurrentTimeStampProperty = true;
-      }
+    if (entityClass.customAttributes !== undefined) {
+      if (entityClass.customAttributes.has("BisCore.ClassHasHandler"))
+        foundClassHasHandler = true;
+      if (entityClass.customAttributes.has("CoreCustomAttributes.ClassHasCurrentTimeStampProperty"))
+        foundClassHasCurrentTimeStampProperty = true;
     }
     assert.isTrue(foundClassHasHandler);
     assert.isTrue(foundClassHasCurrentTimeStampProperty);
-    assert.isDefined(obj.properties.federationGuid);
-    assert.equal(obj.properties.federationGuid.primitiveType, 257);
-    assert.equal(obj.properties.federationGuid.extendedType, "BeGuid");
+    assert.isDefined(entityClass.properties);
+    if (entityClass.properties !== undefined) {
+      for (const prop of entityClass.properties) {
+        if (prop.name === "federationGuid") {
+          assert.isTrue(prop.isPrimitive());
+          assert.equal((prop as PrimitiveOrEnumPropertyBase).extendedTypeName, "BeGuid");
+          break;
+        }
+      }
+    }
   }
 
   it("should get metadata for class", () => {
-    // eslint-disable-next-line @typescript-eslint/no-deprecated
-    const metaData: EntityMetaData = imodel1.getMetaData(Element.classFullName);
+    const metaData = imodel1.schemaContext.getSchemaItemSync(Element.classFullName, EntityClass);
     assert.exists(metaData);
-    checkElementMetaData(metaData);
+    if (metaData !== undefined)
+      checkElementMetaData(metaData);
   });
 
   it("update the project extents", async () => {
@@ -1143,25 +1146,26 @@ describe("iModel", () => {
     assert.isTrue(imodel5.geographicCoordinateSystem!.verticalCRS!.id === "ELLIPSOID");
   });
 
-  // eslint-disable-next-line @typescript-eslint/no-deprecated
-  function checkClassHasHandlerMetaData(obj: EntityMetaData) {
-    assert.isDefined(obj.properties.restrictions);
-    assert.equal(obj.properties.restrictions.primitiveType, 2305);
-    assert.equal(obj.properties.restrictions.minOccurs, 0);
-  }
+  function checkClassHasHandlerMetaData(classToCheck: CustomAttributeClass) {
+    assert.isDefined(classToCheck);
 
-  it("should get metadata for CA class just as well (and we'll see a array-typed property)", () => {
-    // eslint-disable-next-line @typescript-eslint/no-deprecated
-    const metaData: EntityMetaData = imodel1.getMetaData("BisCore:ClassHasHandler");
-    assert.exists(metaData);
-    checkClassHasHandlerMetaData(metaData);
-  });
+    const propertiesArray = classToCheck.properties ? Array.from(classToCheck.properties) : [];
+    assert.equal(propertiesArray.length, 1);
+
+    const restrictionProperty = propertiesArray[0];
+    assert.isDefined(restrictionProperty);
+    assert.equal(restrictionProperty.name, "Restrictions");
+    assert.equal(propertyTypeToString(restrictionProperty.propertyType), "PrimitiveArrayProperty");
+    assert.equal((restrictionProperty as PrimitiveArrayProperty).minOccurs, 0);
+  }
 
   it("should get metadata for CA class just as well (and we'll see a array-typed property)", () => {
-    // eslint-disable-next-line @typescript-eslint/no-deprecated
-    const metaData = imodel1.getMetaData("BisCore:ClassHasHandler");
+    const metaData = imodel1.schemaContext.getSchemaItemSync("BisCore.ClassHasHandler", CustomAttributeClass);
     assert.exists(metaData);
-    checkClassHasHandlerMetaData(metaData);
+    if (metaData !== undefined) {
+      assert.equal(metaData.schemaItemType, SchemaItemType.CustomAttributeClass);
+      checkClassHasHandlerMetaData(metaData);
+    }
   });
 
   it("should exercise ECSqlStatement (backend only)", () => {
@@ -1364,10 +1368,14 @@ describe("iModel", () => {
   });
 
   it("should import schemas", async () => {
-    // eslint-disable-next-line @typescript-eslint/no-deprecated
-    const classMetaData = imodel1.getMetaData("TestBim:TestDocument"); // will throw on failure
-    assert.isDefined(classMetaData.properties.testDocumentProperty);
-    assert.isTrue(classMetaData.properties.testDocumentProperty.primitiveType === PrimitiveTypeCode.Integer);
+    const metaData = await imodel1.schemaContext.getSchemaItem("TestBim:TestDocument", EntityClass);
+    assert.isDefined(metaData);
+    if (metaData !== undefined) {
+      const property = await metaData.getProperty("testDocumentProperty");
+      assert.isDefined(property);
+      if (property !== undefined)
+        assert.isDefined(property.propertyType, propertyTypeToString(PropertyType.Integer));
+    }
   });
 
   it("should do CRUD on models", () => {
@@ -1405,8 +1413,7 @@ describe("iModel", () => {
   it("should create model with custom relationship to modeled element", async () => {
     const testImodel = imodel1;
 
-    // eslint-disable-next-line @typescript-eslint/no-deprecated
-    assert.isDefined(testImodel.getMetaData("TestBim:TestModelModelsElement"), "TestModelModelsElement is expected to be defined in TestBim.ecschema.xml");
+    assert.doesNotThrow(() => testImodel.schemaContext.getSchemaItemSync("TestBim:TestModelModelsElement", EntityClass), "TestModelModelsElement is expected to be defined in TestBim.ecschema.xml");
 
     let newModelId1: Id64String;
     let newModelId2: Id64String;
@@ -1532,8 +1539,7 @@ describe("iModel", () => {
   it("should set EC properties of various types", async () => {
 
     const testImodel = imodel1;
-    // eslint-disable-next-line @typescript-eslint/no-deprecated
-    testImodel.getMetaData("TestBim:TestPhysicalObject");
+    assert.doesNotThrow(() => testImodel.schemaContext.getSchemaItemSync("TestBim:TestPhysicalObject", EntityClass), "TestPhysicalObject is expected to be defined in TestBim.ecschema.xml");
 
     // Create a new physical model
     const [, newModelId] = IModelTestUtils.createAndInsertPhysicalPartitionAndModel(testImodel, Code.createEmpty(), true);
diff --git a/core/backend/src/test/imodel/SchemaXmlImport.test.ts b/core/backend/src/test/imodel/SchemaXmlImport.test.ts
index 78efbf6611f4..2ace402d52b2 100644
--- a/core/backend/src/test/imodel/SchemaXmlImport.test.ts
+++ b/core/backend/src/test/imodel/SchemaXmlImport.test.ts
@@ -9,6 +9,7 @@ import { PhysicalElement, SnapshotDb } from "../../core-backend";
 import { IModelTestUtils } from "../IModelTestUtils";
 import { Logger, LogLevel } from "@itwin/core-bentley";
 import { KnownTestLocations } from "../KnownTestLocations";
+import { EntityClass } from "@itwin/ecschema-metadata";
 
 describe("Schema XML Import Tests", () => {
   let imodel: SnapshotDb;
@@ -35,11 +36,12 @@ describe("Schema XML Import Tests", () => {
 
     await imodel.importSchemaStrings([schemaString]); // will throw an exception if import fails
 
-    // eslint-disable-next-line @typescript-eslint/no-deprecated
-    const testDomainClass = imodel.getMetaData("Test3:Test3Element"); // will throw on failure
+    const testDomainClass = await imodel.schemaContext.getSchemaItem("Test3.Test3Element", EntityClass);
+    assert.isDefined(testDomainClass);
 
-    assert.equal(testDomainClass.baseClasses.length, 1);
-    assert.equal(testDomainClass.baseClasses[0], PhysicalElement.classFullName);
+    assert.isDefined(testDomainClass?.baseClass);
+
+    assert.equal(testDomainClass?.baseClass?.fullName, PhysicalElement.classFullName.replace(":", "."));
   });
 
   it("Schema import for newer ECXml Versions", async () => {
diff --git a/core/backend/src/test/schema/ClassRegistry.test.ts b/core/backend/src/test/schema/ClassRegistry.test.ts
index bc3338951ea8..026ea4557400 100644
--- a/core/backend/src/test/schema/ClassRegistry.test.ts
+++ b/core/backend/src/test/schema/ClassRegistry.test.ts
@@ -6,7 +6,7 @@ import { assert, expect } from "chai";
 import * as sinon from "sinon";
 import * as path from "path";
 import {
-  BisCodeSpec, Code, ConcreteEntityTypes, DefinitionElementProps, ElementAspectProps, ElementProps, EntityMetaData, EntityReferenceSet, ModelProps,
+  BisCodeSpec, Code, ConcreteEntityTypes, DefinitionElementProps, ECJsNames, ElementAspectProps, ElementProps, EntityReferenceSet, ModelProps,
   RelatedElement, RelatedElementProps, RelationshipProps, SchemaState,
 } from "@itwin/core-common";
 import {
@@ -19,6 +19,7 @@ import { Element } from "../../Element";
 import { Schemas } from "../../Schema";
 import { ClassRegistry } from "../../ClassRegistry";
 import { OpenMode } from "@itwin/core-bentley";
+import { EntityClass, NavigationProperty, PrimitiveProperty } from "@itwin/ecschema-metadata";
 
 describe("Class Registry", () => {
   let imodel: SnapshotDb;
@@ -34,47 +35,43 @@ describe("Class Registry", () => {
     imodel?.close();
   });
 
-  it("should verify the Entity metadata of known element subclasses", () => {
+  it("should verify the Entity metadata of known element subclasses", async () => {
     const code1 = new Code({ spec: "0x10", scope: "0x11", value: "RF1.dgn" });
     const el = imodel.elements.getElement(code1);
     assert.exists(el);
     if (el) {
-      // eslint-disable-next-line @typescript-eslint/no-deprecated
-      const metaData: EntityMetaData | undefined = el.getClassMetaData();
+      const metaData = await el.getMetaData();
       assert.exists(metaData);
-      if (undefined === metaData)
-        return;
-      assert.equal(metaData.ecclass, el.classFullName);
+
+      assert.equal(metaData.fullName, el.classFullName.replace(":", "."));
       // I happen to know that this is a BisCore:RepositoryLink
-      assert.equal(metaData.ecclass, RepositoryLink.classFullName);
+      assert.equal(metaData.fullName, RepositoryLink.classFullName.replace(":", "."));
       //  Check the metadata on the class itself
-      assert.isTrue(metaData.baseClasses.length > 0);
-      assert.equal(metaData.baseClasses[0], UrlLink.classFullName);
-      assert.equal(metaData.customAttributes![0].ecclass, "BisCore:ClassHasHandler");
+      assert.isDefined(metaData.baseClass);
+      assert.equal(metaData.baseClass?.fullName, UrlLink.classFullName.replace(":", "."));
+      assert.isTrue(metaData.customAttributes?.has("BisCore.ClassHasHandler"));
       //  Check the metadata on the one property that RepositoryLink defines, RepositoryGuid
-      assert.exists(metaData.properties);
-      assert.isDefined(metaData.properties.repositoryGuid);
-      const p = metaData.properties.repositoryGuid;
-      assert.equal(p.extendedType, "BeGuid");
-      assert.equal(p.customAttributes![1].ecclass, "CoreCustomAttributes:HiddenProperty");
+      assert.isDefined(metaData.properties);
+      const repositoryGuidProperty = await metaData.getProperty("repositoryGuid") as PrimitiveProperty;
+      assert.isDefined(repositoryGuidProperty);
+      assert.equal(repositoryGuidProperty.extendedTypeName, "BeGuid");
+      assert.isTrue(repositoryGuidProperty.customAttributes?.has("CoreCustomAttributes.HiddenProperty"));
     }
     const el2 = imodel.elements.getElement("0x34");
     assert.exists(el2);
     if (el2) {
-      // eslint-disable-next-line @typescript-eslint/no-deprecated
-      const metaData = el2.getClassMetaData();
+      const metaData = await el2.getMetaData();
       assert.exists(metaData);
-      if (undefined === metaData)
-        return;
-      assert.equal(metaData.ecclass, el2.classFullName);
+
+      assert.equal(metaData.fullName, el2.classFullName.replace(":", "."));
       // I happen to know that this is a BisCore.SpatialViewDefinition
-      assert.equal(metaData.ecclass, SpatialViewDefinition.classFullName);
-      assert.isTrue(metaData.baseClasses.length > 0);
-      assert.equal(metaData.baseClasses[0], ViewDefinition3d.classFullName);
-      assert.exists(metaData.properties);
-      assert.isDefined(metaData.properties.modelSelector);
-      const n = metaData.properties.modelSelector;
-      assert.equal(n.relationshipClass, "BisCore:SpatialViewDefinitionUsesModelSelector");
+      assert.equal(metaData.fullName, SpatialViewDefinition.classFullName.replace(":", "."));
+      assert.isDefined(metaData.baseClass);
+      assert.equal(metaData.baseClass?.fullName, ViewDefinition3d.classFullName.replace(":", "."));
+      assert.isDefined(metaData.properties);
+      const modelSelectorProperty = await metaData.getProperty("modelSelector") as NavigationProperty;
+      assert.isDefined(modelSelectorProperty);
+      assert.equal(modelSelectorProperty.relationshipClass.fullName, "BisCore.SpatialViewDefinitionUsesModelSelector");
     }
   });
 
@@ -82,23 +79,22 @@ describe("Class Registry", () => {
     const schemaPathname = path.join(KnownTestLocations.assetsDir, "TestDomain.ecschema.xml");
     await imodel.importSchemas([schemaPathname]); // will throw an exception if import fails
 
-    // eslint-disable-next-line @typescript-eslint/no-deprecated
-    const testDomainClass = imodel.getMetaData("TestDomain:TestDomainClass"); // will throw on failure
+    const testDomainClass = await imodel.schemaContext.getSchemaItem("TestDomain", "TestDomainClass", EntityClass);
+    assert.isDefined(testDomainClass);
 
-    assert.equal(testDomainClass.baseClasses.length, 2);
-    assert.equal(testDomainClass.baseClasses[0], DefinitionElement.classFullName);
-    assert.equal(testDomainClass.baseClasses[1], "TestDomain:IMixin");
+    assert.isDefined(testDomainClass?.baseClass);
+    assert.equal(testDomainClass?.baseClass?.fullName, DefinitionElement.classFullName.replace(":", "."));
 
-    // Ensures the IMixin has been loaded as part of getMetadata call above.
-    // eslint-disable-next-line @typescript-eslint/no-deprecated
-    assert.isDefined(imodel.classMetaDataRegistry.find("TestDomain:IMixin"));
+    assert.isDefined(testDomainClass?.mixins);
+    assert.equal(testDomainClass?.mixins.length, 1);
+    assert.equal(testDomainClass?.mixins[0].fullName, "TestDomain.IMixin");
 
     // Verify that the forEach method which is called when constructing an entity
     // is picking up all expected properties.
     const testData: string[] = [];
-    // eslint-disable-next-line @typescript-eslint/no-deprecated
-    IModelDb.forEachMetaData(imodel, "TestDomain:TestDomainClass", true, (propName) => {
-      testData.push(propName);
+
+    await IModelDb.forEachProperty(imodel, "TestDomain.TestDomainClass", true, (propName, _property) => {
+      testData.push(ECJsNames.toJsName(propName));
     }, false);
 
     const expectedString = testData.find((testString: string) => {
diff --git a/core/common/src/EntityProps.ts b/core/common/src/EntityProps.ts
index a19421d5ca24..25718f3e2c1c 100644
--- a/core/common/src/EntityProps.ts
+++ b/core/common/src/EntityProps.ts
@@ -88,6 +88,7 @@ export enum PrimitiveTypeCode {
 
 /** A callback function to process properties of an Entity
  * @beta
+ * @deprecated in 5.0. Use `PropertyHandler` from `ecschema-metadata` instead.
  */
 export type PropertyCallback = (name: string, meta: PropertyMetaData) => void;
 
diff --git a/core/ecschema-editing/src/Merging/SchemaMerger.ts b/core/ecschema-editing/src/Merging/SchemaMerger.ts
index 6b13e963618f..55b4fa5c0914 100644
--- a/core/ecschema-editing/src/Merging/SchemaMerger.ts
+++ b/core/ecschema-editing/src/Merging/SchemaMerger.ts
@@ -136,7 +136,13 @@ class MergingSchemaContext extends SchemaContext {
     return this._internalContext.getSchema(schemaKey, matchType);
   }
 
-  public override async getSchemaItem<T extends typeof SchemaItem>(schemaItemKey: SchemaItemKey, itemConstructor?: T): Promise<SchemaItem | InstanceType<T> | undefined> {
+  public override async getSchemaItem<T extends typeof SchemaItem>(schemaNameOrKey: string | SchemaItemKey, itemNameOrCtor?: string | T, itemConstructor?: T): Promise<SchemaItem | InstanceType<T> | undefined> {
+    let schemaItemKey: SchemaItemKey;
+    if (typeof schemaNameOrKey === "string")
+      schemaItemKey = new SchemaItemKey(itemNameOrCtor as string, new SchemaKey(schemaNameOrKey));
+    else
+      schemaItemKey = schemaNameOrKey;
+
     const mappedKey = this._nameMappings.resolveItemKey(schemaItemKey);
     if(mappedKey !== undefined) {
       schemaItemKey = mappedKey as SchemaItemKey;
diff --git a/core/ecschema-metadata/src/Context.ts b/core/ecschema-metadata/src/Context.ts
index 86c275faf2a6..a4280de4f5bc 100644
--- a/core/ecschema-metadata/src/Context.ts
+++ b/core/ecschema-metadata/src/Context.ts
@@ -5,6 +5,9 @@
 import { SchemaMatchType } from "./ECObjects";
 import { ECObjectsError, ECObjectsStatus } from "./Exception";
 import { SchemaInfo } from "./Interfaces";
+import type { EntityClass } from "./Metadata/EntityClass";
+import type { Mixin } from "./Metadata/Mixin";
+import { PropertyHandler } from "./Metadata/Property";
 import { MutableSchema, Schema } from "./Metadata/Schema";
 import { SchemaItem } from "./Metadata/SchemaItem";
 import { SchemaItemKey, SchemaKey } from "./SchemaKey";
@@ -60,31 +63,6 @@ export interface ISchemaLocater {
   getSchemaSync(schemaKey: SchemaKey, matchType: SchemaMatchType, context: SchemaContext): Schema | undefined;
 }
 
-/**
- * Base interface for types that allow location of items inside a schema
- * @beta
- */
-export interface ISchemaItemLocater {
-  /**
-   * Gets the schema item from the specified schema if it exists in this [[SchemaContext]].
-   * Will await a partially loaded schema then look in it for the requested item.
-   * @param schemaItemKey The SchemaItemKey identifying the item to return.  SchemaMatchType.Latest is used to match the schema.
-   * @returns The requested schema item.
-   */
-  getSchemaItem(schemaItemKey: SchemaItemKey): Promise<SchemaItem | undefined>;
-  getSchemaItem<T extends typeof SchemaItem>(schemaItemKey: SchemaItemKey, itemConstructor: T): Promise<InstanceType<T> | undefined>;
-
-  /**
-   * Gets the schema item from the specified schema if it exists in this [[SchemaContext]].
-   * Will skip a partially loaded schema and return undefined if the item belongs to that schema. Use the async method to await partially loaded schemas.
-   * @param schemaItemKey The SchemaItemKey identifying the item to return.  SchemaMatchType.Latest is used to match the schema.
-   * @param itemConstructor The constructor of the item to return.
-   * @returns The requested schema item.
-   */
-  getSchemaItemSync(schemaItemKey: SchemaItemKey): SchemaItem | undefined;
-  getSchemaItemSync<T extends typeof SchemaItem>(schemaItemKey: SchemaItemKey, itemConstructor: T): InstanceType<T> | undefined;
-}
-
 /**
  * @internal
  */
@@ -263,7 +241,7 @@ export class SchemaCache implements ISchemaLocater {
  * The context is made up of a group of Schema Locators.
  * @beta
  */
-export class SchemaContext implements ISchemaItemLocater {
+export class SchemaContext {
   private _locaters: ISchemaLocater[];
 
   private _knownSchemas: SchemaCache;
@@ -431,43 +409,102 @@ export class SchemaContext implements ISchemaItemLocater {
 
   /**
    * Gets the schema item from the specified schema if it exists in this [[SchemaContext]].
-   * Will await a partially loaded schema then look in it for the requested item
-   * @param schemaItemKey The SchemaItemKey identifying the item to return.  SchemaMatchType.Latest is used to match the schema.
-   * @param itemConstructor The constructor of the item to return.
-   * @returns The requested schema item
+   * Will await a partially loaded schema then look in it for the requested item.
+   *
+   * @param schemaNameOrKey - The SchemaItemKey identifying the item to return or the name of the schema or the schema item full name.
+   * @param itemNameOrCtor - The name of the item to return or the constructor of the item to return.
+   * @param itemConstructor - The constructor of the item to return.
+   * @returns The requested schema item, or `undefined` if the item could not be found.
+   *
+   * @examples
+   * ```typescript
+   * const schemaItem = await schemaContext.getSchemaItem(new SchemaItemKey("TestElement", new SchemaKey("TestSchema")));
+   * const schemaItemWithCtor = await schemaContext.getSchemaItem(new SchemaItemKey("TestElement", new SchemaKey("TestSchema")), EntityClass);
+   * const schemaItemByName = await schemaContext.getSchemaItem("TestSchema", "TestElement");
+   * const schemaItemByNameWithCtor = await schemaContext.getSchemaItem("TestSchema", "TestElement", EntityClass);
+   * const schemaItemFullName = await schemaContext.getSchemaItem("TestSchema:TestElement", EntityClass);
+   * const schemaItemFullNameWithCtor = await schemaContext.getSchemaItem("TestSchema:TestElement", EntityClass);
+   * const schemaItemFullNameSep = await schemaContext.getSchemaItem("TestSchema.TestElement", EntityClass);
+   * const schemaItemFullNameSepWithCtor = await schemaContext.getSchemaItem("TestSchema.TestElement", EntityClass);
+   * ```
    */
-  public async getSchemaItem(schemaItemKey: SchemaItemKey): Promise<SchemaItem | undefined>
-  public async getSchemaItem<T extends typeof SchemaItem>(schemaItemKey: SchemaItemKey, itemConstructor: T): Promise<InstanceType<T> | undefined>
-  public async getSchemaItem<T extends typeof SchemaItem>(schemaItemKey: SchemaItemKey, itemConstructor?: T): Promise<SchemaItem | InstanceType<T> | undefined> {
-    const schema = await this.getSchema(schemaItemKey.schemaKey, SchemaMatchType.Latest);
-    if (undefined === schema)
+  public async getSchemaItem<T extends typeof SchemaItem>(schemaNameOrKey: SchemaItemKey | string, itemNameOrCtor?: T): Promise<InstanceType<T> | undefined>
+  public async getSchemaItem<T extends typeof SchemaItem>(schemaNameOrKey: SchemaItemKey | string, itemNameOrCtor?: T): Promise<SchemaItem | undefined>
+  public async getSchemaItem<T extends typeof SchemaItem>(schemaNameOrKey: string, itemNameOrCtor: string, itemConstructor?: T): Promise<InstanceType<T> | undefined>
+  public async getSchemaItem<T extends typeof SchemaItem>(schemaNameOrKey: string, itemNameOrCtor: string, itemConstructor?: T): Promise<SchemaItem | undefined>
+  public async getSchemaItem<T extends typeof SchemaItem>(schemaNameOrKey: SchemaItemKey | string, itemNameOrCtor?: T | string, itemConstructor?: T): Promise<SchemaItem | InstanceType<T> | undefined> {
+    let schemaKey: SchemaKey;
+    if (typeof schemaNameOrKey === "string") {
+      const [schemaName, itemName] = SchemaItem.parseFullName(schemaNameOrKey);
+      schemaKey = (!schemaName) ? new SchemaKey(itemName) : new SchemaKey(schemaName);
+    } else {
+      schemaKey = schemaNameOrKey.schemaKey;
+    }
+
+    const schema = await this.getSchema(schemaKey, SchemaMatchType.Latest);
+
+    if (!schema)
       return undefined;
 
-    if(undefined === itemConstructor)
-      return schema.getItem(schemaItemKey.name);
+    if (typeof itemNameOrCtor === "string") // Separate schema and item name arguments with/without an itemConstructor
+      return itemConstructor ? schema.getItem(itemNameOrCtor, itemConstructor) : schema.getItem(itemNameOrCtor);
 
-    return schema.getItem(schemaItemKey.name, itemConstructor);
-  }
+    if (typeof schemaNameOrKey === "string") // Single schema item full name argument with/without an itemConstructor
+      return itemNameOrCtor ? schema.lookupItem(schemaNameOrKey, itemNameOrCtor) : schema.lookupItem(schemaNameOrKey);
+
+    // Schema Item Key with/without an itemConstructor
+    return itemNameOrCtor ? schema.getItem(schemaNameOrKey.name, itemNameOrCtor) : schema.getItem(schemaNameOrKey.name);
+    }
 
   /**
    * Gets the schema item from the specified schema if it exists in this [[SchemaContext]].
    * Will return undefined if the cached schema is partially loaded. Use [[getSchemaItem]] to await until the schema is completely loaded.
-   * @param schemaItemKey The SchemaItemKey identifying the item to return. SchemaMatchType.Latest is used to match the schema.
-   * @param itemConstructor The constructor of the item to return.
-   * @returns The requested schema item
+   *
+   * @param nameOrKey - The SchemaItemKey identifying the item to return or the name of the schema or the schema item full name.
+   * @param nameOrCtor - The name of the item to return or the constructor of the item to return.
+   * @param ctor - The constructor of the item to return.
+   * @returns The requested schema item, or `undefined` if the item could not be found.
+   *
+   * @example
+   * ```typescript
+   * const schemaItem = schemaContext.getSchemaItemSync(new SchemaItemKey("TestElement", new SchemaKey("TestSchema")));
+   * const schemaItemWithCtor = schemaContext.getSchemaItemSync(new SchemaItemKey("TestElement", new SchemaKey("TestSchema")), EntityClass);
+   * const schemaItemByName = schemaContext.getSchemaItemSync("TestSchema", "TestElement");
+   * const schemaItemByNameWithCtor = schemaContext.getSchemaItemSync("TestSchema", "TestElement", EntityClass);
+   * const schemaItemFullName = schemaContext.getSchemaItemSync("TestSchema:TestElement", EntityClass);
+   * const schemaItemFullNameWithCtor = schemaContext.getSchemaItemSync("TestSchema:TestElement", EntityClass);
+   * const schemaItemFullNameSep = schemaContext.getSchemaItemSync("TestSchema.TestElement", EntityClass);
+   * const schemaItemFullNameSepWithCtor = schemaContext.getSchemaItemSync("TestSchema.TestElement", EntityClass);
+   * ```
    */
-  public getSchemaItemSync(schemaItemKey: SchemaItemKey): SchemaItem | undefined
-  public getSchemaItemSync<T extends typeof SchemaItem>(schemaItemKey: SchemaItemKey, itemConstructor: T): InstanceType<T> | undefined
-  public getSchemaItemSync<T extends typeof SchemaItem>(schemaItemKey: SchemaItemKey, itemConstructor?: T): SchemaItem | InstanceType<T> | undefined {
-    const schema = this.getSchemaSync(schemaItemKey.schemaKey, SchemaMatchType.Latest);
-    if (undefined === schema)
+  public getSchemaItemSync<T extends typeof SchemaItem>(schemaNameOrKey: SchemaItemKey | string, itemNameOrCtor?: T): InstanceType<T> | undefined
+  public getSchemaItemSync<T extends typeof SchemaItem>(schemaNameOrKey: SchemaItemKey | string, itemNameOrCtor?: T): SchemaItem | undefined
+  public getSchemaItemSync<T extends typeof SchemaItem>(schemaNameOrKey: string, itemNameOrCtor: string, itemConstructor?: T): InstanceType<T> | undefined
+  public getSchemaItemSync<T extends typeof SchemaItem>(schemaNameOrKey: string, itemNameOrCtor: string, itemConstructor?: T): SchemaItem | undefined
+  public getSchemaItemSync<T extends typeof SchemaItem>(schemaNameOrKey: SchemaItemKey | string, itemNameOrCtor?: T | string, itemConstructor?: T): SchemaItem | InstanceType<T> | undefined {
+    let schemaKey: SchemaKey;
+    if (typeof schemaNameOrKey === "string") {
+      const [schemaName, itemName] = SchemaItem.parseFullName(schemaNameOrKey);
+      schemaKey = (!schemaName) ? new SchemaKey(itemName) : new SchemaKey(schemaName);
+    } else {
+      schemaKey = schemaNameOrKey.schemaKey;
+    }
+
+    const schema = this.getSchemaSync(schemaKey, SchemaMatchType.Latest);
+    if (!schema)
       return undefined;
 
-    if(undefined === itemConstructor)
-      return schema.getItemSync(schemaItemKey.name);
+    // Separate schema and item name arguments with/without an itemConstructor
+    if (typeof itemNameOrCtor === "string") {
+      return itemConstructor ? schema.getItemSync(itemNameOrCtor, itemConstructor) : schema.getItemSync(itemNameOrCtor);
+    }
 
-    return schema.getItemSync(schemaItemKey.name, itemConstructor);
-  }
+    if (typeof schemaNameOrKey === "string") // Single schema item full name argument with/without an itemConstructor
+      return itemNameOrCtor ? schema.lookupItemSync(schemaNameOrKey, itemNameOrCtor) : schema.lookupItemSync(schemaNameOrKey);
+
+    // Schema Item Key with/without an itemConstructor
+    return itemNameOrCtor ? schema.getItemSync(schemaNameOrKey.name, itemNameOrCtor) : schema.getItemSync(schemaNameOrKey.name);
+    }
 
   /**
    * Iterates through the items of each schema known to the context.  This includes schemas added to the
@@ -489,4 +526,50 @@ export class SchemaContext implements ISchemaItemLocater {
   public getKnownSchemas(): Schema[] {
     return this._knownSchemas.getAllSchemas();
   }
+
+  /**
+   * Attempt to get metadata for a schema item. This method will load the metadata from the schema context if necessary.
+   * If the metadata cannot be found or loaded, it returns `undefined` instead of throwing an error.
+   *
+   * @param classFullName The full name of the class in the format "SchemaName:SchemaItemName".
+   * @param itemConstructor The constructor of the schema item to return.
+   * @returns The requested schema item metadata, or `undefined` if the metadata cannot be found or loaded.
+   *
+   * @example
+   * ```typescript
+   * const entityClass = schemaContext.tryGetSchemaItem("BisCore:Element", EntityClass);
+   * ```
+   */
+  public tryGetSchemaItem<T extends typeof SchemaItem>(classFullName: string, itemConstructor: T): InstanceType<T> | undefined {
+    try {
+      return this.getSchemaItemSync(classFullName, itemConstructor);
+    } catch {
+      return undefined;
+    }
+  }
+
+  public async forEachProperty<T extends typeof SchemaItem>(classFullName: string, wantSuper: boolean, func: PropertyHandler, itemConstructor: T, includeCustom: boolean = true): Promise<void> {
+    const { EntityClass: entityClass } = await import("./Metadata/EntityClass");
+    const { Mixin: mixin } = await import("./Metadata/Mixin");
+
+    const promises: Promise<void>[] = [];
+
+    const metaData = this.getSchemaItemSync(classFullName, itemConstructor) as EntityClass | Mixin;
+    if (metaData.properties !== undefined) {
+      for (const property of metaData.properties) {
+        if (includeCustom || !property.customAttributes?.has(`BisCore.CustomHandledProperty`))
+          func(property.name, property);
+      }
+    }
+
+    if (wantSuper && metaData.baseClass !== undefined)
+      promises.push(this.forEachProperty(metaData.baseClass.fullName, wantSuper, func, entityClass, includeCustom));
+
+    if (metaData instanceof entityClass && metaData.mixins !== undefined) {
+      for (const mixinClass of metaData.mixins)
+        promises.push(this.forEachProperty(mixinClass.fullName, wantSuper, func, mixin, includeCustom));
+    }
+
+    await Promise.all(promises);
+  }
 }
diff --git a/core/ecschema-metadata/src/ECObjects.ts b/core/ecschema-metadata/src/ECObjects.ts
index 706df343d722..75981d655ba6 100644
--- a/core/ecschema-metadata/src/ECObjects.ts
+++ b/core/ecschema-metadata/src/ECObjects.ts
@@ -485,7 +485,7 @@ export function isSupportedSchemaItemType(value: SchemaItemType, supported: Supp
     value === SchemaItemType.StructClass ||
     value === SchemaItemType.CustomAttributeClass ||
     value === SchemaItemType.RelationshipClass)) {
-      return true;;
+      return true;
   }
 
   if(supported === AbstractSchemaItemType.SchemaItem) {
diff --git a/core/ecschema-metadata/src/Metadata/Property.ts b/core/ecschema-metadata/src/Metadata/Property.ts
index a9a032f81ed3..05eb5accb3b2 100644
--- a/core/ecschema-metadata/src/Metadata/Property.ts
+++ b/core/ecschema-metadata/src/Metadata/Property.ts
@@ -337,6 +337,11 @@ export abstract class Property implements CustomAttributeContainerProps {
   }
 }
 
+/** A callback function to process properties of an Entity
+ * @beta
+ */
+export type PropertyHandler = (name: string, property: Property) => void;
+
 /** @beta */
 export abstract class PrimitiveOrEnumPropertyBase extends Property {
   protected _extendedTypeName?: string;
diff --git a/core/ecschema-metadata/src/ecschema-metadata.ts b/core/ecschema-metadata/src/ecschema-metadata.ts
index 46877eee33a5..e270d289cd36 100644
--- a/core/ecschema-metadata/src/ecschema-metadata.ts
+++ b/core/ecschema-metadata/src/ecschema-metadata.ts
@@ -26,7 +26,7 @@ export { Mixin } from "./Metadata/Mixin";
 export * from "./Metadata/OverrideFormat";
 export { Phenomenon } from "./Metadata/Phenomenon";
 export {
-  Property, PrimitiveProperty, PrimitiveArrayProperty, EnumerationProperty, StructProperty,
+  Property, PropertyHandler, PrimitiveProperty, PrimitiveArrayProperty, EnumerationProperty, StructProperty,
   StructArrayProperty, EnumerationArrayProperty, NavigationProperty, AnyArrayProperty, AnyEnumerationProperty,
   AnyPrimitiveProperty, AnyProperty, AnyStructProperty, ArrayProperty, PrimitiveOrEnumPropertyBase,
 } from "./Metadata/Property";
diff --git a/core/ecschema-metadata/src/test/Context/SchemaContext.test.ts b/core/ecschema-metadata/src/test/Context/SchemaContext.test.ts
index b83a9ee80c43..1629f752ebb9 100644
--- a/core/ecschema-metadata/src/test/Context/SchemaContext.test.ts
+++ b/core/ecschema-metadata/src/test/Context/SchemaContext.test.ts
@@ -9,7 +9,10 @@ import { SchemaCache, SchemaContext } from "../../Context";
 import { SchemaMatchType } from "../../ECObjects";
 import { ECObjectsError } from "../../Exception";
 import { Schema } from "../../Metadata/Schema";
-import { SchemaKey } from "../../SchemaKey";
+import { SchemaItemKey, SchemaKey } from "../../SchemaKey";
+import { EntityClass } from "../../Metadata/EntityClass";
+import { SchemaItem } from "../../Metadata/SchemaItem";
+import { CustomAttributeClass, Format, KindOfQuantity, Mixin, Phenomenon, RelationshipClass, StructClass, Unit, UnitSystem } from "../../ecschema-metadata";
 
 const assert = chai.assert;
 const expect = chai.expect;
@@ -141,4 +144,163 @@ describe("Schema Context", () => {
     expect(schemas[0].schemaKey.matches(schema1.schemaKey)).to.be.true;
     expect(schemas[1].schemaKey.matches(schema2.schemaKey)).to.be.true;
   });
+
+  describe("getSchemaItem (sync and async)", () => {
+    const context = new SchemaContext();
+    type TestCase<T extends typeof SchemaItem> = [number, string, T, boolean];
+
+    const testCases: TestCase<typeof SchemaItem>[] = [
+      [1, "TestEntityClass", EntityClass, true],
+      [2, "TestMixin", Mixin, true],
+      [3, "TestCustomAttributeClass", CustomAttributeClass, true],
+      [4, "TestStructClass", StructClass, true],
+      [5, "TestRelationshipClass", RelationshipClass, true],
+      [6, "TestUnitSystem", UnitSystem, true],
+      [7, "TestPhenomenon", Phenomenon, true],
+      [8, "TestUnit", Unit, true],
+      [9, "TestKoQ", KindOfQuantity, true],
+      [10,"TestFormat", Format, true],
+
+      [11, "TestEntityClass", Mixin, false],
+      [12, "TestMixin", EntityClass, false],
+      [13, "TestCustomAttributeClass", StructClass, false],
+      [14, "TestStructClass", EntityClass, false],
+      [15, "TestRelationshipClass", EntityClass, false],
+      [16, "TestUnitSystem", Unit, false],
+      [17, "TestPhenomenon", UnitSystem, false],
+      [18, "TestUnit", Phenomenon, false],
+      [19, "TestKoQ", Unit, false],
+      [20, "TestFormat", KindOfQuantity, false],
+    ];
+
+    before(async () => {
+      const schemaJson = {
+        $schema: "https://dev.bentley.com/json_schemas/ec/32/ecschema",
+        name: "TestSchema",
+        version: "1.0.0",
+        alias: "ts",
+        items: {
+          testEntityClass: { schemaItemType: "EntityClass", label: "TestEntityClass", description: "An example entity class.", },
+          testMixin: { schemaItemType: "Mixin", appliesTo: "TestSchema.TestEntityClass", },
+          testCustomAttributeClass: { schemaItemType: "CustomAttributeClass", label: "TestCustomAttributeClass", appliesTo: "Any", },
+          testStructClass: { schemaItemType: "StructClass", name: "TestStructClass", modifier: "sealed", },
+          testRelationshipClass: {
+            schemaItemType: "RelationshipClass",
+            modifier: "None",
+            strength: "Referencing",
+            strengthDirection: "Forward",
+            source: { multiplicity: "(0..*)", roleLabel: "refers to", polymorphic: true, constraintClasses: ["TestSchema.TestEntityClass",], },
+            target: { multiplicity: "(0..*)", roleLabel: "is referenced by", polymorphic: true, constraintClasses: ["TestSchema.TestEntityClass",],},
+          },
+
+          testUnitSystem: { schemaItemType: "UnitSystem", },
+          testPhenomenon: { schemaItemType: "Phenomenon", definition: "TestPhenomenon", },
+          testUnit: { schemaItemType: "Unit", unitSystem: "TestSchema.TestUnitSystem", phenomenon: "TestSchema.TestPhenomenon", definition: "TestUnit",},
+          testKoQ: { schemaItemType: "KindOfQuantity", description: "Description of koq", persistenceUnit: "TestSchema.TestUnit", relativeError: 1.23, },
+
+          testFormat: {
+            schemaItemType: "Format",
+            type: "Fractional",
+            precision: 8,
+            formatTraits: ["KeepSingleZero", "KeepDecimalPoint", "ShowUnitLabel",],
+            decimalSeparator: ",",
+            thousandSeparator: ".",
+            uomSeparator: "",
+            composite: { spacer: "", units: [{ name: "TestSchema.TestUnit", label: "'",},],},
+          },
+        },
+      };
+      const schema = await Schema.fromJson(schemaJson, context);
+      assert.isDefined(schema);
+    })
+
+    it("getSchemaItem with different schema item constructors", async () => {
+      for (const [testCaseNumber, schemaItemName, schemaItemType, shouldSucceed] of testCases) {
+        const schemaItem = await context.getSchemaItem(`TestSchema.${schemaItemName}`, schemaItemType);
+
+        if (shouldSucceed) {
+          assert.isDefined(schemaItem, `Failed to get schema item ${schemaItemName} for test case number ${testCaseNumber}`);
+          assert.isTrue(schemaItem instanceof schemaItemType, `Expected schema item to be of type ${schemaItemType.name} for test case number ${testCaseNumber}`);
+        } else {
+          assert.isUndefined(schemaItem, `Unexpectedly found schema item ${schemaItemName} for test case number ${testCaseNumber}. Should have returned undefined`);
+        }
+      }
+    });
+
+    function testSchemaItem(testCaseNumber: number, schemaItem: SchemaItem | undefined, expectedSchemaItemType: typeof SchemaItem) {
+      assert.isDefined(schemaItem, `Failed to get schema item for test case number ${testCaseNumber}`);
+      assert.isTrue(schemaItem instanceof expectedSchemaItemType, `Expected schema item to be of type ${expectedSchemaItemType.name} for test case number ${testCaseNumber}`);
+    }
+
+    it("getSchemaItem with different function arguments", async () => {
+      for (const [testCaseNumber, schemaItemName, schemaItemType, shouldSucceed] of testCases) {
+        if (!shouldSucceed)
+          continue;
+
+        const getSchemaItemArgs = [
+          new SchemaItemKey(schemaItemName, new SchemaKey("TestSchema")),
+          ["TestSchema", schemaItemName],
+          `TestSchema.${schemaItemName}`,
+          `TestSchema:${schemaItemName}`,
+        ];
+
+        for (const argument of getSchemaItemArgs) {
+          let schemaItem: any
+          if (Array.isArray(argument)) {
+            schemaItem = await context.getSchemaItem(argument[0], argument[1]);
+            testSchemaItem(testCaseNumber, schemaItem, schemaItemType);
+            schemaItem = await context.getSchemaItem(argument[0], argument[1], schemaItemType);
+            testSchemaItem(testCaseNumber, schemaItem, schemaItemType);
+          } else {
+            schemaItem = await context.getSchemaItem(argument);
+            testSchemaItem(testCaseNumber, schemaItem, schemaItemType);
+            schemaItem = await context.getSchemaItem(argument, schemaItemType);
+            testSchemaItem(testCaseNumber, schemaItem, schemaItemType);
+          }
+        }
+      }
+    });
+
+    it("getSchemaItemSync with different schema item constructors", () => {
+      for (const [testCaseNumber, schemaItemName, schemaItemType, shouldSucceed] of testCases) {
+        const schemaItem = context.getSchemaItemSync(`TestSchema.${schemaItemName}`, schemaItemType);
+
+        if (shouldSucceed) {
+          assert.isDefined(schemaItem, `Failed to get schema item ${schemaItemName} for test case number ${testCaseNumber}`);
+          assert.isTrue(schemaItem instanceof schemaItemType, `Expected schema item to be of type ${schemaItemType.name} for test case number ${testCaseNumber}`);
+        } else {
+          assert.isUndefined(schemaItem, `Unexpectedly found schema item ${schemaItemName} for test case number ${testCaseNumber}. Should have returned undefined`);
+        }
+      }
+    });
+
+    it("getSchemaItemSync with different function arguments", () => {
+      for (const [testCaseNumber, schemaItemName, schemaItemType, shouldSucceed] of testCases) {
+        if (!shouldSucceed)
+          continue;
+
+        const getSchemaItemArgs = [
+          new SchemaItemKey(schemaItemName, new SchemaKey("TestSchema")),
+          ["TestSchema", schemaItemName],
+          `TestSchema.${schemaItemName}`,
+          `TestSchema:${schemaItemName}`,
+        ];
+
+        for (const argument of getSchemaItemArgs) {
+          let schemaItem: any
+          if (Array.isArray(argument)) {
+            schemaItem = context.getSchemaItemSync(argument[0], argument[1]);
+            testSchemaItem(testCaseNumber, schemaItem, schemaItemType);
+            schemaItem = context.getSchemaItemSync(argument[0], argument[1], schemaItemType);
+            testSchemaItem(testCaseNumber, schemaItem, schemaItemType);
+          } else {
+            schemaItem = context.getSchemaItemSync(argument);
+            testSchemaItem(testCaseNumber, schemaItem, schemaItemType);
+            schemaItem = context.getSchemaItemSync(argument, schemaItemType);
+            testSchemaItem(testCaseNumber, schemaItem, schemaItemType);
+          }
+        }
+      }
+    });
+  });
 });
diff --git a/docs/changehistory/NextVersion.md b/docs/changehistory/NextVersion.md
index 6190906ab9b4..ebda37b30a5e 100644
--- a/docs/changehistory/NextVersion.md
+++ b/docs/changehistory/NextVersion.md
@@ -189,6 +189,36 @@ Can also be attached as a map-layer:
   - [IModelDb.forEachMetaData()]($backend)
   - [MetaDataRegistry]($backend)
 
+#### Deprecated metadata retrieval methods
+
+The `IModelDb.getMetaData(classFullName: string)` method has been deprecated in version 5.0. This method was used to get metadata for a class and would load the metadata from the iModel into the cache, if necessary.
+
+**Replacement:**
+
+Use the async `schemaContext.getSchemaItem` method from the `ecschema-metadata` package instead. This method provides a more comprehensive and type-safe way to retrieve schema items.
+
+**Example Replacement:**
+
+```typescript
+// Deprecated method
+iModelDb.getMetaData("SchemaName:ClassName");
+
+// Replacement using schemaContext with a schema key/schemaName-itemName combination/schema item full name
+await iModelDb.schemaContext.getSchemaItem("SchemaName:ClassName");
+await iModelDb.schemaContext.getSchemaItem("SchemaName", "ClassName");
+await iModelDb.schemaContext.getSchemaItem("SchemaName:ClassName");
+await iModelDb.schemaContext.getSchemaItem("SchemaName.ClassName");
+```
+
+Similarly, other functions to retrieve metadata also have replacements
+
+| **Removed**            | **Replacement function from `@itwin/ecschema-metadata`** | Usage |
+| -----------------------| ----------------------------------------------------------------------| - |
+| `tryGetMetaData`       | Use `tryGetSchemaItem` from `@itwin/ecschema-metadata` instead. | schemaContext.tryGetSchemaItem("BisCore:Element", EntityClass) |
+| `forEachMetaData`      | Use async `forEachProperty` from `@itwin/ecschema-metadata` instead.  | await schemaContext.forEachProperty("BisCore:Element", true, callback, true, EntityClass) |
+| `forEachProperty`      | Use async `executeForEachProperty` from `@itwin/core-backend` instead.  | await entity.executeForEachProperty(callback) |
+
+
 ### @itwin/core-frontend
 
 - Deprecated [SelectionSet]($core-frontend)-related APIs:
diff --git a/domains/analytical/backend/src/test/AnalyticalSchema.test.ts b/domains/analytical/backend/src/test/AnalyticalSchema.test.ts
index 2ce1ba7a0f12..c4d33101dbe7 100644
--- a/domains/analytical/backend/src/test/AnalyticalSchema.test.ts
+++ b/domains/analytical/backend/src/test/AnalyticalSchema.test.ts
@@ -123,9 +123,8 @@ describe("AnalyticalSchema", () => {
     const elementId: Id64String = iModelDb.elements.insertElement(elementProps);
     // test forEachProperty and PropertyMetaData.isNavigation
     const element: GeometricElement3d = iModelDb.elements.getElement(elementId);
-    const metadata = await element.getMetaData();
-    (await metadata.getProperties()).forEach((property) => {
-      switch (property.name) {
+    await element.executeForEachProperty((propName, property) => {
+      switch (propName) {
         case "model":
         case "category":
         case "typeDefinition":
@@ -134,9 +133,9 @@ describe("AnalyticalSchema", () => {
         case "codeValue":
         case "userLabel":
           assert.isFalse(property.isNavigation());
-          break;
       }
-    });
+    }, true);
+
     // test typeDefinition update scenarios
     assert.isTrue(Id64.isValidId64(elementId));
     assert.isTrue(Id64.isValidId64(iModelDb.elements.getElement<GeometricElement3d>(elementId).typeDefinition!.id), "Expect valid typeDefinition.id");


Reviews:
user: login: rschili
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T08:48:52Z
user: login: rschili
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T09:01:51Z
user: login: rschili
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T15:45:36Z

Review comments:
diff_hunk: @@ -449,6 +427,35 @@ export class SchemaContext implements ISchemaItemLocater {
     return schema.getItem(schemaItemKey.name, itemConstructor);
   }
 
+  /**
+   * Gets the schema item from the specified schema by its name if it exists in this [[SchemaContext]].
+   * Will await a partially loaded schema then look in it for the requested item
+   *
+   * @param schemaName The name of the schema containing the item.
+   * @param itemName The name of the item to find.
+   * @returns The requested schema item, or `undefined` if the item could not be found.
+   *
+   * @example
+   * ```typescript
+   * await schemaContext.getSchemaItemByName("BisCore", "Element");
+   * await schemaContext.getSchemaItemByName("BisCore", "Element", EntityClass);
+   * await schemaContext.getSchemaItemByName("Units", "THOUSAND_SQ_FT");
+   * await schemaContext.getSchemaItemByName("Units", "THOUSAND_SQ_FT", Unit);
+   * ```
+   */
+  public async getSchemaItemByName(schemaName: string, itemName: string): Promise<SchemaItem | undefined>
path: core/ecschema-metadata/src/Context.ts
user: login: rschili
body: I have been thinking about this.
Even though I despise the typescript method overload syntax, I believe we should try and make everything into a single method, and give it a very short name, so it's easiest for callers.

Something like this should work:

```ts
  public async getItem(schemaItemKey: SchemaItemKey): Promise<SchemaItem | undefined>
  public async getItem<T extends typeof SchemaItem>(schemaItemKey: SchemaItemKey, itemConstructor: T): Promise<InstanceType<T> | undefined>
  public async getItem(schemaName: string, itemName: string): Promise<SchemaItem | undefined>
  public async getItem<T extends typeof SchemaItem>(schemaName: string, itemName: string, itemConstructor: T): Promise<InstanceType<T> | undefined>
  public async getItem<T extends typeof SchemaItem>(nameOrKey: SchemaItemKey | string, nameOrCtor?: T | string, ctor?: T): Promise<SchemaItem | InstanceType<T> | undefined> {
    let schema: Schema | undefined;
    if(typeof nameOrKey === "string") {
      schema = await this.getSchema(new SchemaKey(nameOrKey), SchemaMatchType.Latest);
    } else {
      schema = await this.getSchema(nameOrKey.schemaKey, SchemaMatchType.Latest);
    }

    if (undefined === schema)
      return undefined;

    if(typeof nameOrCtor === "string") {
      if(ctor === undefined)
        return schema.getItem(nameOrCtor);
      return schema.getItem(nameOrCtor, ctor);
    } else {
      if (typeof nameOrKey !== "string") {
        if (nameOrCtor === undefined)
          return schema.getItem(nameOrKey.name);
        return schema.getItem(nameOrKey.name, nameOrCtor);
      }
    }

    return undefined; // or throw error? This should never happen
  }
```

created_at: 2025-03-12T08:48:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -489,4 +525,73 @@ export class SchemaContext implements ISchemaItemLocater {
   public getKnownSchemas(): Schema[] {
     return this._knownSchemas.getAllSchemas();
   }
+
+  /**
+   * Get metadata for a schema item. This method will load the metadata from the schema context if necessary.
+   *
+   * @param classFullName The full name of the class in the format "SchemaName:SchemaItemName".
+   * @param itemConstructor The constructor of the schema item to return.
+   * @returns The requested schema item metadata.
+   * @throws [[ECObjectsError]] if the metadata cannot be found or loaded.
+   *
+   * @example
+   * ```typescript
+   * const entityClass = schemaContext.getSchemaItemMetaData("BisCore:Element", EntityClass);
+   * const customAttributeClass = schemaContext.getSchemaItemMetaData("BisCore:SchemaHasBehavior", CustomAttributeClass);
+   * const relationshipClass = schemaContext.getSchemaItemMetaData("BisCore:ElementOwnsChildElements", RelationshipClass);
+   * ```
+   */
+  public getSchemaItemMetaData<T extends typeof SchemaItem>(classFullName: string, itemConstructor: T): InstanceType<T> {
path: core/ecschema-metadata/src/Context.ts
user: login: rschili
body: Hmm, I do not like this one. It feels like clutter.
I'd rather not have a fullname overload and instead have a nice public method which splits a fullname into two strings.
So instead of
`x.getSchemaItemMetaData("myschema:myclass");`
people could call
`x.getItem(...parseFullName("mySchema:myClass");`

Assuming the parseFullName method looks like this:
```ts
  public parseFullName(input: string): [string, string] {
    const [first, second] = input.split(":");
    return [first, second];
  }
The parseFullName is just a sketch. The real one needs to support colon, dot and make sure there are only 2 results...
```
Or if we absolutely need to stick with this, better call it getItemByFullName(). Everything here is MetaData, so that previous name is ambiguous.

created_at: 2025-03-12T09:01:51Z
author_association: CONTRIBUTOR

PR comments:
user: login: mergify[bot]
created_at: 2025-03-13T10:10:05Z
body: This pull request is now in conflicts. Could you fix it @RohitPtnkr1996? 🙏
To fixup this pull request, you can check out it locally. See documentation: https://help.github.com/articles/checking-out-pull-requests-locally/
author_association: CONTRIBUTOR
user: login: RohitPtnkr1996
created_at: 2025-03-13T12:36:48Z
body: /azp run itwinjs-core
author_association: CONTRIBUTOR
user: login: azure-pipelines[bot]
created_at: 2025-03-13T12:36:56Z
body: <samp>
No pipelines are associated with this pull request.<br>

</samp>
author_association: NONE