Writing metadata by hand
You write metadata yourself when the server can't provide it: a non-.NET backend, a third-party API, or a service you can't change. You can also add a few types by hand to a store that otherwise came from the server.
You rarely have to write the JSON format described in Metadata in depth. MetadataStore.addEntityType takes compact configuration objects and fills in the rest.
A worked example
This defines part of Northwind: Category, Supplier and Product entity types, and a Location complex type.
import {
MetadataStore, DataService, EntityManager,
DataType, AutoGeneratedKeyType, Validator,
} from 'breeze-client';
const namespace = 'Northwind.Models';
// 1. The store. It takes the default naming convention, camelCase.
const store = new MetadataStore();
// 2. The service. hasServerMetadata: false - don't ask the server for metadata.
const dataService = new DataService({
serviceName: 'breeze/Northwind',
hasServerMetadata: false,
});
store.addDataService(dataService);
// 3. The types.
store.addEntityType({
shortName: 'Location',
namespace,
isComplexType: true,
dataProperties: {
address: { maxLength: 60 },
city: { maxLength: 15 },
postalCode: { maxLength: 10 },
},
});
store.addEntityType({
shortName: 'Category',
namespace,
autoGeneratedKeyType: AutoGeneratedKeyType.Identity,
defaultResourceName: 'Categories',
dataProperties: {
categoryID: { dataType: DataType.Int32, isPartOfKey: true, isNullable: false },
categoryName: { maxLength: 15, isNullable: false },
},
});
store.addEntityType({
shortName: 'Supplier',
namespace,
autoGeneratedKeyType: AutoGeneratedKeyType.Identity,
defaultResourceName: 'Suppliers',
dataProperties: {
supplierID: { dataType: DataType.Int32, isPartOfKey: true, isNullable: false },
companyName: { maxLength: 40, isNullable: false },
location: { complexTypeName: `Location:#${namespace}`, isNullable: false },
phone: { maxLength: 24, validators: [Validator.phone()] },
},
navigationProperties: {
products: { entityTypeName: 'Product', isScalar: false, associationName: 'Supplier_Products' },
},
});
store.addEntityType({
shortName: 'Product',
namespace,
autoGeneratedKeyType: AutoGeneratedKeyType.Identity,
defaultResourceName: 'Products',
dataProperties: {
productID: { dataType: DataType.Int32, isPartOfKey: true, isNullable: false },
productName: { maxLength: 40, isNullable: false },
categoryID: { dataType: DataType.Int32 },
supplierID: { dataType: DataType.Int32 },
unitPrice: { dataType: DataType.Decimal },
discontinued: { dataType: DataType.Boolean, isNullable: false },
},
navigationProperties: {
category: { entityTypeName: 'Category', associationName: 'Product_Category', foreignKeyNames: ['categoryID'] },
supplier: { entityTypeName: 'Supplier', associationName: 'Supplier_Products', foreignKeyNames: ['supplierID'] },
},
});
// 4. A manager that uses them.
const em = new EntityManager({ dataService, metadataStore: store });The entities behave as if the metadata had come from the server:
const beverages = em.createEntity('Category', { categoryID: 1, categoryName: 'Beverages' });
const supplier = em.createEntity('Supplier', { companyName: 'Exotic Liquids' });
const chai = em.createEntity('Product', { productName: 'Chai', categoryID: 1 });
chai.getProperty('category') === beverages; // true: wired from categoryID
chai.setProperty('supplier', supplier);
supplier.getProperty('products').includes(chai); // true: the inverse is kept in step
supplier.getProperty('location').setProperty('city', 'London');Queries go to breeze/Northwind as usual. Because the store already has metadata for that service, Breeze doesn't request any.
The pieces
Store and naming convention
Breeze works out each property's server name when you add the type, using the store's naming convention. A store takes the default convention, NamingConvention.camelCase, when it is created. To use another, pass it to the constructor — new MetadataStore({ namingConvention: NamingConvention.none }) — before adding types. With camelCase, you write client names (productName) and Breeze sends ProductName to the server. See Naming conventions.
DataService
The DataService identifies the server. hasServerMetadata: false stops Breeze from requesting <serviceName>/Metadata. Pass the same DataService to the EntityManager, or add it to the store with addDataService: a manager given only a serviceName uses the store's DataService for that service. Only if the store has none does it create a new one, which expects server metadata. Adding the service to the store also marks its metadata as present (store.hasMetadataFor('breeze/Northwind') is true).
Types
addEntityType accepts a configuration object, or an EntityType or ComplexType you constructed yourself. A configuration object with isComplexType: true becomes a complex type.
dataProperties and navigationProperties can be a map, where each key is the property name (as above), or an array of DataProperty / NavigationProperty instances.
The configuration objects take the same property names as the JSON format; the tables in Metadata in depth list them all. Three differences:
JSON (importMetadata) | Config object (addEntityType) | |
|---|---|---|
autoGeneratedKeyType | "Identity" | AutoGeneratedKeyType.Identity |
dataType | "Int32" | DataType.Int32 (a name string also works) |
validators | [{ "name": "phone" }] | [Validator.phone()] or [Validator.fromJSON({ name: 'phone' })] |
The defaults you will lean on: dataType is String, isNullable is true, isScalar is true, and autoGeneratedKeyType is None (the client assigns keys).
Every entity type needs at least one key property (isPartOfKey: true). Adding one without a key throws. So does adding a type whose qualified name is already in the store.
Navigation properties
entityTypeNameis the related type. A short name such as'Product'gets the declaring type's namespace, becomingProduct:#Northwind.Models.associationNamepairs the two ends of a relationship.Supplier.productsandProduct.suppliershare'Supplier_Products', so Breeze keeps both sides in step.Product.categoryhas no inverse; its association name appears only once.foreignKeyNamesgoes on the end that holds the foreign key, hereProduct. The collection end (Supplier.products) doesn't repeat it.- If the dependent type has no navigation property back, give the collection end
invForeignKeyNames: the names of the foreign key properties on the related type.
You can add types in any order. A navigation property to a type that isn't there yet is resolved when that type arrives.
Complex types
Define the complex type with isComplexType: true and refer to it from a data property with complexTypeName. Use the qualified name there: unlike entityTypeName, it is not qualified for you. Add isScalar: false for an array of complex objects. See Complex properties.
Validators are not inferred
Metadata from the server already lists validators for each property: required, maxLength, int32 and so on. Breeze doesn't create them from isNullable, maxLength or dataType. So hand-written metadata has only the validators you list, and a non-nullable property without Validator.required() accepts an empty value.
Add them yourself, or derive them from the metadata once all the types are in:
import { ComplexType, DataType, EntityType, Validator } from 'breeze-client';
function inferValidators(type: EntityType | ComplexType) {
for (const dp of type.dataProperties) {
const add = (v: Validator | undefined) => {
if (v && !dp.validators.some(x => x.name === v.name)) dp.validators.push(v);
};
if (!dp.isNullable) add(Validator.required());
if (!(dp.dataType instanceof DataType)) continue; // complex property
if (dp.dataType === DataType.String) {
if (dp.maxLength) add(Validator.maxLength({ maxLength: dp.maxLength }));
} else {
add(dp.dataType.validatorCtor?.()); // int32, number, bool, date...
}
}
}
store.getEntityTypes().forEach(inferValidators);See Validation.
Resource names
A query names a resource (EntityQuery.from('Products')), not a type. For a query against the server that doesn't matter, but a query against the cache has to know which type the resource returns.
Each type's defaultResourceName is registered for you. To make more names work, register them:
store.setEntityTypeForResourceName('Product', 'Product');
em.executeQueryLocally(EntityQuery.from('Product')); // now works tooA resource name maps to exactly one type. If two types share a short name in different namespaces, use the qualified type name as the second argument.
Building types from classes
Instead of configuration objects you can construct the metadata classes directly, which is handy when generating metadata in a loop:
import { EntityType, DataProperty, DataType } from 'breeze-client';
const personType = new EntityType({ shortName: 'Person', namespace: 'App' });
personType.addProperty(new DataProperty({
name: 'id', dataType: DataType.Guid, isPartOfKey: true, isNullable: false,
}));
personType.addProperty(new DataProperty({ name: 'name', maxLength: 100 }));
store.addEntityType(personType);You can go on calling addProperty after addEntityType, until the first entity of that type is attached to a manager. After that the type is frozen, and addProperty throws.
Loading metadata from a JSON file
Fetching metadata from the server is an asynchronous round trip before anything else can happen. If you'd rather have it at startup, which also helps offline use and makes tests fast and synchronous, ship it as a JSON file:
import { MetadataStore, DataService, EntityManager } from 'breeze-client';
import metadata from './northwind-metadata.json';
const store = new MetadataStore();
store.importMetadata(metadata);
const dataService = new DataService({
serviceName: '/breeze/NorthwindIBModel',
hasServerMetadata: false,
});
const em = new EntityManager({ dataService, metadataStore: store });To produce the file, save the response of <serviceName>/Metadata from your server, or call exportMetadata() on a store that already has the metadata. Server output carries no naming convention, so the store's own is used. An exportMetadata() file carries one, and an empty store adopts it.
Bundlers (Vite, webpack, esbuild) import JSON as shown. TypeScript needs "resolveJsonModule": true. In Node without a bundler, add an import attribute: import metadata from './northwind-metadata.json' with { type: 'json' };.
The file is a snapshot. When the server model changes, it goes stale, and the errors that follow can be confusing. Regenerate it as part of your build, for example with a script that fetches /Metadata from a running server and writes the file.