Migrating from 2.x
Most applications need only a handful of changes. This page lists all of them.
The canonical version of this list lives in UPGRADE.md in the repository, and is kept in step with the code.
1. Install
npm install breeze-client # 3.0.0One package, one tag. 2.x published the same package under latest, mjs, cjs and umd. If your package.json says "breeze-client": "cjs", change it to "^3.0.0".
ESM only. require('breeze-client') no longer works, there is no UMD bundle, and the <script src="breeze.debug.js"> + global breeze style of deployment is gone. Use a bundler, or an import map. Node 20 or later.
TypeScript 4.1 or later, and a bundler that reads ES2022. The typings use template literal types, which 4.0 cannot parse; the published JavaScript uses class fields, which webpack 4's parser cannot. Both are worth checking before you start, because each fails immediately and with a message that does not name the cause. An application on Angular 11 or earlier is on webpack 4 and will need the package transpiled down a level to build.
Subpath imports are spelled the same as before:
import { EntityManager } from 'breeze-client';
import { DataServiceWebApiAdapter } from 'breeze-client/adapter-data-service-webapi';2. Removed: Knockout, jQuery, AngularJS, OData
| Removed | What to do |
|---|---|
adapter-model-library-ko | Move models to the backing-store adapter (the default), or stay on 2.x |
adapter-ajax-jquery | Remove it — Breeze 3 calls fetch directly |
adapter-ajax-angularjs | AngularJS reached end of life in 2022. Stay on 2.x |
adapter-data-service-odata | Stay on 2.x — Breeze's JSON query format is the supported path |
adapter-uri-builder-odata | Use adapter-uri-builder-json with a Breeze .NET server |
Entities always have plain properties
The model library used to decide the shape of an entity. With Knockout gone there is only the backing store, so a property is just a property:
order.freight; // 2.x with Knockout: order.freight()
order.freight = 12.5; // 2.x with Knockout: order.freight(12.5)getProperty and setProperty still work, on entities and complex objects alike. To drive a UI, bind to the plain values and use the events in Change tracking to find out when they change.
What else went with OData
Predicate.toODataFragmentis gone. It was never a core method: the OData URI builder added it toPredicate.prototypewhen you imported that adapter. UsetoJSON().- Requests no longer carry
$filter,$orderby,$selector$expand. The JSON URI builder is the only one left, so if you inspect network traffic, expect Breeze's JSON query format. A server that understands only OData query syntax is not supported — stay on 2.x for those.
CSDL / EDMX metadata is no longer parsed
MetadataStore.importMetadata() used to detect a schema property and parse CSDL — the OData / EDMX metadata format. That is gone, along with DataType.fromEdmDataType. Passing CSDL to importMetadata() now throws a clear error rather than silently importing nothing.
This matters if you fetch metadata from an OData $metadata endpoint or an older Breeze WebApi2 + EF6 server. Breeze .NET Core servers emit Breeze JSON metadata and are unaffected.
An object with a schema property and no structuralTypes makes importMetadata() throw This looks like CSDL (OData / EDMX) metadata, which breeze-client 3 does not read, prefixed by Unable to either parse or import metadata. So pointing Breeze 3 at such a server fails on the first metadata fetch with that message rather than importing nothing.
If your metadata is CSDL: move the server to Breeze .NET Core, which emits native JSON. Or, while still on 2.x, load the CSDL once, call exportMetadata(), and check the resulting JSON in — Breeze 3 can import that file.
The Breeze Labs Metadata-Helper is gone
The 2.x docs used breeze.metadata-helper.js for hand-written metadata, with abbreviated attribute names (type, max, fk) and convention-based defaults. There is no v3 version. The native API is nearly as short — it takes property maps, qualifies navigation type names for you and has sensible defaults. See Writing metadata by hand.
Angular
There is no adapter-ajax-httpclient in the v3 package. Breeze 3 works in an Angular application as it is, and makes its requests with fetch. To keep them going through HttpClient, import httpClientFetch from breeze-client/adapter-angular-httpclient — an optional extension that takes the 2.x adapter's place. Angular covers setting it up, change detection and auth headers.
3. Importing an adapter no longer registers it
// 2.x: the import alone registered the adapter
import 'breeze-client/adapter-data-service-webapi';v3 modules do not touch global state on import. For the standard adapters this does not matter, because they need no registration at all. With nothing registered, Breeze uses DataServiceWebApiAdapter, UriBuilderJsonAdapter and ModelLibraryBackingStoreAdapter, and sends requests through fetch. You can delete the side-effect imports.
2.x startup code that then initialized those adapters by name keeps working, even though nothing registered them:
config.initializeAdapterInstance('modelLibrary', 'backingStore', true);
config.initializeAdapterInstance('uriBuilder', 'json', true);
config.initializeAdapterInstance('dataService', 'webApi', true);
config.initializeAdapterInstance('ajax', 'fetch', true);The names 'backingStore', 'json', 'webApi' and 'fetch' resolve to the defaults; 'fetch' gives an ajax adapter that sends requests through config.fetch. A name that is neither registered nor a default still throws Unregistered adapter.
A custom adapter still has to be registered, by your own code or with configureBreeze({ dataService: MyAdapter }). Do it at startup, before you create an EntityManager — see Default adapters.
4. You no longer need an ajax adapter
Breeze 3 makes every request through a fetch function — globalThis.fetch unless you supply your own — so a normal setup has no ajax adapter:
configureBreeze({ fetch: myFetch }); // optional: auth headers, retry, loggingYou don't have to change anything. AjaxFetchAdapter, configureBreeze({ ajax }), config.initializeAdapterInstance('ajax', 'fetch') and the adapter's defaultSettings and requestInterceptor all still work, and a registered ajax adapter is used in preference to config.fetch. They are deprecated: move to a fetch function when convenient — see Supplying your own transport.
A custom ajax adapter still works the same way. So does a data service adapter that calls this.ajaxImpl.ajax(...) directly; _ajax() is the promise-based alternative.
AbstractDataServiceAdapter is promise-based: fetchMetadata, executeQuery and saveChanges are async and return native promises, sharing one _ajax helper. A 2.x subclass that overrode only _prepareSaveBundle, _prepareSaveResult and jsonResultsAdapter works unchanged. An override that returned a Q promise must return a native one.
A side effect: registration order no longer matters. In 2.x the data service adapter needed the ajax adapter registered first, and threw Unable to find ajax adapter for dataservice adapter 'webApi' otherwise.
5. Class constructors require new
Predicate('CompanyName', 'StartsWith', 'B'); // 2.x: worked. v3: throws.
new Predicate('CompanyName', 'StartsWith', 'B'); // works
Predicate.create('CompanyName', 'startsWith', 'B') // works, preferredv3 ships real ES2022 classes, and a class constructor cannot be invoked as a plain function. In 2.x this appeared to work only because the test harness compiled the library down to ES5; anyone consuming the mjs build was already affected.
Fix by adding new, or using the static factory where one exists.
6. The default naming convention is now camelCase
Breaking change. In 2.x the default was NamingConvention.none: client property names were the server's names unless you set a convention. Breeze 3 defaults to NamingConvention.camelCase, which suits a Breeze .NET server: CompanyName on the server is companyName on the client. A MetadataStore takes the default when it is created, so every store, and every EntityManager that creates its own, uses camelCase unless told otherwise. With the default adapters, a Breeze .NET server needs no configuration at all:
import { EntityManager } from 'breeze-client';
const em = new EntityManager('/breeze/Northwind');If you already set camelCase, nothing changes.
NamingConvention.camelCase.setAsDefault()orconfigureBreeze({ namingConvention: NamingConvention.camelCase })is now redundant but harmless; you can delete it.If you relied on
none— your server already sends the property names the client should use, as a Node/Sequelize-style server does, or its metadata names are already camelCase — set it explicitly at startup, before you create anyMetadataStoreorEntityManager:tsimport { configureBreeze, NamingConvention } from 'breeze-client'; configureBreeze({ namingConvention: NamingConvention.none }); // or: NamingConvention.none.setAsDefault();Without it, PascalCase server names reach your code camel-cased, so code that reads
CompanyNamefinds nothing. Server names that are already camelCase do not round-trip (camelCaseturnscompanyNameback intoCompanyName), so metadata either fails to load with adoes not roundtrip properlyerror, or Breeze sends the server upper-cased names.
Metadata that names a naming convention still sets it when imported into an empty store, so a store loaded from an exportMetadata() file keeps the convention it was exported with. NamingConvention.none appears there as 'noChange'. See Naming conventions.
configureBreeze takes namingConvention directly. There is no breeze. global, and no NamingConvention.instance — the current default is NamingConvention.defaultInstance.
7. Typed configuration (optional, recommended)
For the standard adapters you need neither form: they are the defaults. When you do register an adapter of your own, configureBreeze replaces the stringly-typed pairs:
// still works, now deprecated
config.registerAdapter('dataService', MyWebApiAdapter);
config.initializeAdapterInstance('dataService', 'myWebApi', true);
// preferred
configureBreeze({ dataService: MyWebApiAdapter });Misspell an adapter name in the old form and you get a runtime error; in the new form it does not compile. The old API is not scheduled for removal. configureBreeze itself is optional: use it for a custom adapter, a custom fetch, or the naming convention.
If you call config.initializeAdapterInstances from TypeScript, you can drop any cast: its argument is now typed as adapter names, { ajax: 'fetch', dataService: 'webApi' }.
noEval is removed, and Breeze no longer evaluates strings
config.noEval and configureBreeze({ noEval }) are gone. Remove them — passing noEval to configureBreeze is a compile error, and config.noEval reads as undefined.
Nothing replaces them, because there is nothing left to switch off. The flag existed because 2.x built entity constructors from strings, Function('return function Order(){}')(), so a type had a readable name in a debugger — and probed for that ability at startup with Function('') inside a try, which a strict Content Security Policy reports as a violation even though the failure was caught.
Constructors are now named with Object.defineProperty, which gives the same result with no dynamic code. Breeze contains no eval and no new Function, so it runs under a policy without 'unsafe-eval' and reports nothing at startup.
8. Types are stricter
v3 builds under strictNullChecks and noImplicitAny. Two declarations changed in ways you may notice:
getPropertyandsetPropertyare no longer optional, onEntityor onComplexObject. They never were in practice — the model library adapter installs them on every entity — and marking them optional forced needless null checks on callers. Drop the!or?.you may have needed. A class thatimplements Entitynow has to list them, withdeclareso the field does not shadow what Breeze installs — see Class fields anddeclare— or extendEntityBase, which declares them for it.EntityQuery.wherePredicate,EntityAspect.hasTempKeyandValidationError.propertyNameare now optional, which is what they always were at runtime.
These are type-only changes; nothing behaves differently.
9. The callback arguments are deprecated
The async methods have always taken an optional success and failure callback alongside the promise they return. That pair is now deprecated on EntityManager.executeQuery, saveChanges and fetchMetadata, EntityQuery.execute, EntityAspect.loadNavigationProperty, MetadataStore.fetchMetadata and a relation array's load.
Nothing has stopped working, and none of these calls change behaviour. Your editor will strike the call through, and the callbacks will be removed in a future major version:
// deprecated
em.executeQuery(query, data => render(data.results), err => show(err));
// supported
try {
const data = await em.executeQuery(query);
render(data.results);
} catch (err) {
show(err);
}The promise form is the only one that was ever fully supported: the save-queuing mixin, for instance, has always ignored the callback arguments.
The callback types — Callback, ErrorCallback, QuerySuccessCallback and QueryErrorCallback — are deprecated along with them.
10. Promises are native
Every async method returns a native Promise. Use await, or .then/.catch. The Q idiom that 2.x code often used does not exist:
em.saveChanges().fail(handler); // 2.x with Q — TypeError in v3
em.saveChanges().catch(handler); // v3Nothing else in the query or save contract changed.
11. Save errors arrive as a problem details document
A Breeze .NET server now returns RFC 9457application/problem+json, with type, title, status and detail. Before 3.0 it sent { Code, Message, StackTrace, EntityErrors } with no content type of its own, always included the stack trace, and set Code to 0 for anything that was not an EntityErrorsException.
A client needs no changes. Breeze reads either spelling, e.message comes from detail falling back to title, and the capitalised members are still sent by default so an application on an older client reads the error unchanged. It matters only if you parse the response yourself. See Error handling.
What has not changed
The rest of the API is intended to be source-compatible. EntityManager, EntityQuery, Predicate, MetadataStore, EntityType, EntityAspect, Validator, DataType, the BreezeEnum types, save and query options, and the event model all keep their names, shapes and semantics.
If you hit a difference not listed here, it is a bug — please file an issue.
What the upgrade does change, without your code changing, is what it costs to run: a cached entity takes half the memory it did under 2.x, and queries and importEntities are about 1.5× quicker. See Performance vs 2.x.
Still there, just not in the API reference
The reference documents the surface an application uses. Two dozen exported names carry @hidden, which keeps a declaration out of the reference but not out of the package: they are in the published .d.ts, they compile, and they run. Every one of them was @hidden in 2.x too — nothing was hidden in 3.0. They are listed here because the natural way to check whether something survived the upgrade is to search the reference for it, and these are the names that are present but will not be found.
For writing an adapter — you need these only if you implement one:
| For | Names |
|---|---|
a DataServiceAdapter | AbstractDataServiceAdapter, MappingContext, SaveContext, SaveBundle |
a UriBuilderAdapter | OrderByClause, SelectClause, ExpandClause |
a ModelLibraryAdapter | makeRelationArray, makePrimitiveArray, makeComplexArray |
| registering either | AdapterCtor, AdapterRegistration |
For walking a predicate with Predicate.visit: the tree nodes UnaryPredicate, BinaryPredicate, AndOrPredicate, AnyAllPredicate, LitExpr, FnExpr and PropExpr, and the Visitor, VisitContext and ExpressionContext types a visitor is written against.
assertParam and assertConfig went the other way
They are more available than in 2.x, not less. 2.x marked them @hidden @internal, and @internal strips a declaration from the published .d.ts — so although the runtime export was there, TypeScript could not see it. In 3.0 they are @hidden only: still out of the reference, but exported by name from breeze-client and typed. Param itself stays off the barrel; reach it as breeze.Param, which is typed, or let assertParam build one.
Five core members that exist only at runtime
assert-param.ts, config.ts and event.ts each assign a member onto core at import time, for 2.x code that reached them through the breeze.core global. They are not on the core type, in 3.0 or in 2.x, so TypeScript rejects all five even though the call works. Use the real export instead:
| At runtime only | Use instead |
|---|---|
core.assertParam, core.assertConfig | the exports of the same name |
core.Param | breeze.Param |
core.config | the exported config |
core.Event | the exported BreezeEvent |
Two more pieces of 2.x scaffolding are still in place: window.breeze, set when the module runs unbundled, and the empty promises.IPromiseService that breeze-bridge2-angular imports.
One caveat carried over from 2.x unchanged: breeze.assertParam and breeze.assertConfig are null, not functions — the lines that would have assigned them are commented out in both versions. Call assertParam directly.
breeze.version is "3.0.0", so code that checks it - for example, to take a 2.x-only path - now sees Breeze 3.
Fixed along the way
Long-standing defects, all present in 2.x:
JsonResultsAdapterlost its type brand. It declared_$typeNamewithout TypeScript'sdeclaremodifier, so under ES2022 class-field semantics the constructor overwrote the prototype value withundefined, andEntityQuery.using(myJsonResultsAdapter)failed to recognise its argument.- The fetch adapter set
referrer: 'client', which is the browser default and redundant there, but which Node'sfetchrejects outright — making Breeze unusable with the fetch adapter under Node. config.initializeAdapterInstancesalways threw. It validated its argument, copied it onto the globalconfig, and then iterated every property ofconfigrather than of the argument — passing values such asfunctionRegistrytoinitializeAdapterInstanceas adapter names. Nothing tested it. It now initializes exactly the adapters named, in dependency order, and leavesconfigalone.- More types are exported. The config objects public constructors take (
EntityTypeConfig,QueryOptionsConfig,SaveOptionsConfig, …), event args, callbacks,SaveError,ImportResultand the types adapter authors implement against were used in public signatures but could not be imported by name. They can now. Type-only; nothing changes at runtime. - The fetch adapter could leave a query pending for ever. A 200 response whose body was not JSON (an HTML login page from a proxy, say) failed inside the adapter without reaching either callback. Every path now settles, and an unreadable body reports the real HTTP status. A throw inside a data service adapter callback now rejects the promise too.
- Calling
enableSaveQueuingtwice hungsaveChanges, including turning it off withenableSaveQueuing(em, false). It looked up a misspelled property, so each call wrappedsaveChangesagain. - Save queuing sent temporary keys to the server. With the save-queuing mixin, a foreign key set while its parent row was still being inserted kept its temporary (negative) value in the follow-up save, because the fixup compared the type that declares the foreign key instead of the type it points at. Only a self-referencing key ever matched. Against SQL Server the queued save failed with a foreign key constraint violation.
removeValidationError(validator)removed nothing. It now removes every error that validator produced on the entity.- An entity whose key was the string
__proto__could not be found again. The cache's key index was a plain object, so writing that one key ranObject.prototype's inherited setter and stored nothing: the entity was attached,getEntityByKeyreturnednull, a re-query added a second copy, and detaching it threw internal error - entity cannot be found in group. - Working with many changed entities at once is no longer quadratic.
acceptChanges,rejectChangesanddetachEntityeach asked the manager to work out afresh whether anything was still dirty, which meant walking the whole cache — once per entity. Over 40,000 entitiesacceptChangestook 8.5 seconds; it takes 33 ms.getEntityGraphwith a two-level expand was the same shape: 5.1 seconds over 8,000 orders and their 24,000 details, now 10 ms. See Performance. BreezeEvent.isEnabledignored its object argument, andEntityState.isDeletedOrDetached()returned false forDeleted.- Local projections named nested paths differently from the server. Only the first dot was replaced, and the client names were used:
order.customer.companyNamecame back asorder_customer.companyName. A local projection now uses the name the server gives the path, passed through the naming convention, so it matches a remote one:order_Customer_CompanyNamewith camelCase. See Projections. - A string assigned to a
DateOnlyproperty stayed a string. It is now parsed as a local date. config.getAdapterInstancewas missing from the published type declarations. It was tagged@internaland the build strips internal members, so TypeScript code calling it needed a cast. It is now public.
Fixes that change what your code sees
These correct long-standing defects, but code that worked around them, or relied on them, may notice.
Queries
- Local
substringtakes a length, as the server does:substring(s, start, length). It used JavaScript'ssubstring(start, end), so a cache query could return different rows from the same query on the server. { value, isLiteral: false }in a where clause now makes the value a property expression, as documented. It was ignored.withParametersvalues are sent once, as plain query-string arguments — the copy the .NET server reads. They used to be sent inside the JSON query as well; a query with only parameters now requestsResource?name=valuewith no JSON.- Untyped results fall back to the resource's entity type. When a query's resource maps to an entity type, result nodes the JSON results adapter cannot type (no
$type) now become entities of that type instead of plain objects. The fallback was computed and then dropped. Typed nodes, projections andtoType()queries are unaffected. FilterQueryOp.IsTypeOfis removed. The server has no such operator, so it could never work; code that referenced it now fails to compile instead of throwing at runtime.EntityQuery.toJSON()now includesusePost.
breeze.core utilities
Five
corehelpers are removed, all written for a JavaScript that no longer needs them and none of them used by Breeze itself any more. If you call one, the replacement is a built-in:Removed Instead core.requireLibNothing. It found jQuery, Knockout or the OData client through browser globals or AMD require, and all four are gone from v3. Use animport.core.isES5SupportedNothing — it always answered true.core.isNumeric(n)Number.isFinite(Number(n))core.titleCase(s)Your own, or a library — it was one regex. core.getArray(obj, name)core.getMapArray(map, key), orobj[name] ??= []Six
corehelpers are deprecated, because the language now has each of them. They still work, and an editor will strike them through:Deprecated Instead core.hasOwnProperty(obj, key)Object.hasOwn(obj, key)core.arraySlice(arr, start, end)arr.slice(start, end)core.arrayFlatMap(arr, fn)arr.flatMap(fn)core.getUuid()crypto.randomUUID()core.stringStartsWith(s, prefix)s.startsWith(prefix)core.stringEndsWith(s, suffix)s.endsWith(suffix)Each now calls the built-in, so behaviour is unchanged and two of them are faster —
endsWithby 2.3x,getUuidby about 30x, which is worth a few percent of creating an entity with a Guid key.crypto.randomUUID()is also a cryptographic source where the old implementation usedMath.random().Two things to know before you switch:
- The string helpers are null-tolerant and the built-ins are not.
core.stringStartsWith(null, "a")isfalse, wherenull.startsWith("a")throws; andcore.stringStartsWith("abc", null)istrue, where the built-in looks for the text"null". Guard the null yourself. crypto.randomUUID()needs a secure context in a browser — it is unavailable over plain HTTP.core.getUuid()falls back to the old implementation there, so if you serve over HTTP, keep using it or supply your own fallback.
An
Object.createpolyfill went with the five removals, so importing Breeze no longer writes to a global. Everything elsecoreexposes is unchanged.- The string helpers are null-tolerant and the built-ins are not.
Metadata and data types
- More server type names are understood.
TimeSpanmaps toDataType.Time,TimeOnlyto the newDataType.TimeOnly(a"HH:mm:ss"string),ChartoString, andSByteand the unsigned integers to the next wider integer type. They all used to becomeStringsilently. A name the client still does not know imports asDataType.Undefined, with the original inrawTypeName, and logs a warning. DateOnlyhas the date validator, and sorts correctly across the year 2000.- Comparison options you choose win.
localQueryComparisonOptionsnamed in imported metadata no longer override options passed to theMetadataStoreor set withsetAsDefault().LocalQueryComparisonOptionsno longer requiresusesSql92CompliantStringComparison(defaulttrue). new EntityManager({ serviceName, metadataStore })uses the store'sDataServicefor that service, with itshasServerMetadataand adapters, instead of building a new one.importMetadatafor a new type with nodataPropertiesthrows a clear error instead of aTypeError.
Validation
- An error you add with
addValidationErrornow stops a save.validateEntity()returnedtruewhile one stood, andsaveChangessent the entity anyway;validateProperty()ignored one about its property. All three now treat it as an error until you remove it. Errors from the server still do not stop a save — every save clears them first. If you added errors as warnings that were not meant to block, remove them before saving, or keep warnings somewhere other than the entity's validation errors. See Which errors stop a save. - A save stopped by an added error beside an ordinary validation failure threw a
TypeError—Cannot read properties of undefined (reading 'name')— in place of the validation error and itsentityErrors. It now rejects properly, naming the added error by its key inerrorName. - A server validation error goes when the property it is about is edited, instead of staying until the next save.
getValidationErrors('someProperty')includes an error added with only apropertyNamein its context, which it used to leave out.
Errors and adapters
- A failed request's
ServerErrornow carriesstatusText,bodyandurl, which it always declared. A status-0 failure's message now says the server may not be running, and keeps the underlying message. - A save whose response has no body rejects with a clear error instead of a
TypeError. - A failed query no longer also raises an unhandled promise rejection.
ChangeRequestInterceptor.oneTimeis honoured.- The
interfaceInitializedevent reports the realisDefault.
Types
EntityManagerConfig.keyGeneratoris removed. It was always rejected at runtime; usekeyGeneratorCtor.setPropertyis typed to return a value, so chaining compiles.JsonResultsAdapterConfig.visitNodeis required in the type, as it always was at runtime.- Published for adapter authors: the static
AbstractDataServiceAdapter.makeHttpErrorand the protected_createChangeRequestInterceptor. DataService.useJsonpis deprecated. It has no effect on Breeze’s own transport, which has no JSONP support; a registered (deprecated) ajax adapter can still act on it.