Metadata in depth
This page describes the Breeze native JSON metadata format. The Breeze .NET server sends it, MetadataStore.exportMetadata() writes it, and MetadataStore.importMetadata() reads it. It is the only format Breeze 3 reads; see Metadata if you have CSDL.
The excerpts below come from the Northwind metadata fixtures in the Breeze test suite. (That model's namespace really is Foo.)
The top level
{
"metadataVersion": "1.0.5",
"namingConvention": "camelCase",
"localQueryComparisonOptions": "caseInsensitiveSQL",
"structuralTypes": [
{ "shortName": "Location", "namespace": "Foo", "isComplexType": true, "dataProperties": [ /* ... */ ] },
{ "shortName": "Customer", "namespace": "Foo", "autoGeneratedKeyType": "Identity", /* ... */ },
{ "shortName": "Order", "namespace": "Foo", "autoGeneratedKeyType": "Identity", /* ... */ }
],
"resourceEntityTypeMap": {
"Customers": "Customer:#Foo",
"Orders": "Order:#Foo"
}
}| Key | Meaning |
|---|---|
metadataVersion | Format version. If present, it must equal MetadataStore.metadataVersion ("1.0.5"), or importMetadata throws. |
namingConvention | Name of a registered NamingConvention: "camelCase" or "noChange". An empty store adopts it. A store that already has types throws if it differs from its own. |
localQueryComparisonOptions | Name of a registered LocalQueryComparisonOptions. "caseInsensitiveSQL" is the default. Ignored if the store's own were chosen on the client, passed to its constructor or made the default with setAsDefault(): those win. Otherwise the same rule as namingConvention. |
dataServices | Serialized DataService objects (serviceName, hasServerMetadata, adapterName, uriBuilderName, jsonResultsAdapter, useJsonp). Each is added to the store, replacing any with the same serviceName. |
structuralTypes | The entity and complex types. |
resourceEntityTypeMap | Resource name to qualified entity type name. Merged into the store's map. |
Nothing else is read. The .NET server also sends enumTypes (and altMetadata if a server subclass supplies it), and the client ignores both. The server sends no namingConvention, so the store keeps its own.
Type names
A type is identified by its qualified name, shortName:#namespace: Order:#Foo. Every reference to another type in the JSON (entityTypeName, complexTypeName, baseTypeName, the values of resourceEntityTypeMap) uses the qualified name. In code you can usually use the short name, as long as it is unique in the store.
Entity types
{
"shortName": "Order",
"namespace": "Foo",
"autoGeneratedKeyType": "Identity",
"defaultResourceName": "Orders",
"dataProperties": [
{
"name": "orderID",
"dataType": "Int32",
"isNullable": false,
"defaultValue": 0,
"isPartOfKey": true,
"validators": [{ "name": "required" }, { "name": "int32" }]
},
{
"name": "customerID",
"dataType": "Guid",
"validators": [{ "name": "guid" }]
}
// ...
],
"navigationProperties": [
{
"name": "customer",
"entityTypeName": "Customer:#Foo",
"isScalar": true,
"associationName": "Customer_Orders",
"foreignKeyNames": ["customerID"]
},
{
"name": "orderDetails",
"entityTypeName": "OrderDetail:#Foo",
"isScalar": false,
"associationName": "OrderDetail_Order",
"invForeignKeyNames": ["orderID"]
}
]
}| Property | Notes |
|---|---|
shortName | Required. |
namespace | Defaults to "". |
isComplexType | true makes this a complex type. |
baseTypeName | Qualified name of the base entity type. The base may come later in the array. |
isAbstract | Default false. |
autoGeneratedKeyType | "None" (the default: the client sets the key), "Identity" (the database does), or "KeyGenerator" (a server-side scheme). |
defaultResourceName | The resource Breeze uses when it builds a query for this type itself, such as for EntityAspect.loadNavigationProperty or a query from an EntityKey. It is also added to the resource map. |
dataProperties | Required; may be empty. |
navigationProperties | Optional. |
validators | Entity-level validators. |
custom | Anything you like; see Custom metadata. |
An entity type must have at least one data property with isPartOfKey: true, unless it is abstract. importMetadata throws otherwise.
Data properties
| Property | Default | Notes |
|---|---|---|
name | Client-side name. You need either name or nameOnServer; Breeze derives the other with the store's naming convention. | |
nameOnServer | Server-side name. The .NET server sends only this; exportMetadata writes name. | |
dataType | "String" | A DataType name (list below). Omit it for complex properties. |
complexTypeName | Qualified name of a complex type. Makes this a complex property. | |
isScalar | true | false for an array. |
isNullable | true | |
defaultValue | The value for a new entity, parsed according to the data type. A non-nullable property without one gets the data type's default. | |
isPartOfKey | false | |
isUnmapped | false | A client-only property with no counterpart in the server model. |
isSettable | true | |
concurrencyMode | "Fixed" marks a concurrency property, such as a row version. | |
maxLength | For strings. This does not create a validator; the server lists one in validators. | |
validators | [] | See Validators. |
displayName | Label for the property, used in validation messages. | |
enumType | Qualified name of the server-side enum type, for enum properties. | |
rawTypeName | Server type name, present when dataType is "Undefined". Also set on import when the server's dataType is a name Breeze does not know. | |
custom | See Custom metadata. |
The data type names are String, Int64, Int32, Int16, Byte, Decimal, Double, Single, DateOnly, DateTime, DateTimeOffset, Time, TimeOnly, Boolean, Guid, Binary and Undefined. See Date and time for how the date and time types behave.
Some .NET type names are accepted too, and read as the nearest client type: TimeSpan as Time; Char, and NHibernate's AnsiString, AnsiChar and StringClob, as String; SByte as Int16; UInt16 as Int32; UInt32 and UInt64 as Int64.
A name Breeze does not know is imported as Undefined, with the name in rawTypeName, and Breeze logs a warning once per name. The property's values are passed through unconverted. The DataProperty constructor, by contrast, throws for a name it does not know.
A concurrency property looks like this:
{
"name": "rowVersion",
"dataType": "Int32",
"concurrencyMode": "Fixed",
"validators": [{ "name": "int32" }]
}Complex properties
A complex property names its type with complexTypeName instead of giving a dataType. With isScalar: false it holds an array of complex objects:
{
"nameOnServer": "Location",
"complexTypeName": "Location:#Models",
"isNullable": false,
"validators": [{ "name": "required" }]
},
{
"nameOnServer": "Roles",
"complexTypeName": "Role:#Models",
"isNullable": true,
"isScalar": false
}Complex types
A complex type has isComplexType: true and data properties, but no key and no navigation properties:
{
"shortName": "Location",
"namespace": "Models",
"isComplexType": true,
"dataProperties": [
{
"nameOnServer": "City",
"dataType": "String",
"maxLength": 60,
"validators": [{ "maxLength": 60, "name": "maxLength" }]
}
]
}It accepts shortName, namespace, dataProperties, validators and custom. If you give it navigationProperties, they are ignored.
Navigation properties
| Property | Default | Notes |
|---|---|---|
name / nameOnServer | As for data properties. | |
entityTypeName | Required. Qualified name of the related type. | |
isScalar | true | false if it returns a collection. |
associationName | Links the two ends of a relationship. Both navigation properties carry the same value. | |
foreignKeyNames | [] | Foreign key properties on this type. Used on the dependent end. |
foreignKeyNamesOnServer | [] | The same, as server names. Give one or the other. |
invForeignKeyNames | [] | Foreign key properties on the related type. Used on the principal end. |
invForeignKeyNamesOnServer | [] | The same, as server names. |
validators | [] | |
displayName | ||
custom | See Custom metadata. |
Take the Customer_Orders association. Order.customer is the dependent end: it is scalar and names the foreign key on Order. Customer.orders is the principal end: it returns a collection and names the same key, which lives on the related type, in invForeignKeyNames:
// on Customer
{
"name": "orders",
"entityTypeName": "Order:#Foo",
"isScalar": false,
"associationName": "Customer_Orders",
"invForeignKeyNames": ["customerID"]
}
// on Order
{
"name": "customer",
"entityTypeName": "Customer:#Foo",
"isScalar": true,
"associationName": "Customer_Orders",
"foreignKeyNames": ["customerID"]
}Breeze pairs the two by associationName and keeps them in step. It uses the foreign keys to connect entities as they arrive in the cache. When the dependent end has no navigation property of its own, the principal's invForeignKeyNames is what lets Breeze maintain the collection. See Navigation properties.
Validators
Each entry in a validators array is an object with a name and any options that validator takes:
"validators": [
{ "name": "required" },
{ "name": "maxLength", "maxLength": 40 }
]The name must belong to a registered validator. The built-in ones (required, maxLength, int32, guid, date and so on) are always registered. An unknown name throws Unable to locate a validator named: ... during import. For your own validators, call Validator.register(validator) or Validator.registerFactory(factory, name) before you import metadata that refers to them. See Validation.
How importMetadata behaves
store.importMetadata(json); // json may be an object or a string
const store2 = MetadataStore.importMetadata(jsonString); // a new store- The input is not modified.
- A type that is already in the store is left as it is, so importing the same metadata twice is harmless. With the second argument,
allowMerge, set totrue, only itscustomvalues are merged in. See Custom metadata. - A type that is not in the store is created, with or without
allowMerge, so it needsdataProperties. Without themimportMetadatathrowsUnable to import type '…'. - A derived type may appear before its base type.
- The method returns the store, so calls can be chained.
exportMetadata
exportMetadata() returns the store as a JSON string in the same format, with the keys shown in The top level. Properties are written with their client name, and the store's namingConvention is included so that a later import reads them the same way. custom values are included too.
const json = em.metadataStore.exportMetadata();
localStorage.setItem('metadata', json);
// later
const store = MetadataStore.importMetadata(localStorage.getItem('metadata')!);EntityManager.exportEntities includes the metadata by default; see Export and import.
Services that don't describe their data
Many web services return plain JSON with no type information:
[{ "Id": 1, "Name": "blue" }, { "Id": 2, "Name": "red" }]You may know these are Color entities, but Breeze doesn't. It returns them to you as-is: plain objects that are not cached, tracked or validated. To get entities, you need two things:
- Metadata for
Color, written by hand or loaded from a file. - A
JsonResultsAdapterthat tells Breeze which JSON objects areColors.