This topic concentrates on the model object’s entity nature, in particular how the entity is tracked during its lifetime on the client. You’ll learn about the EntityAspect
property through which the developer can access and control the state of the entity within the Breeze system.
Code snippets on this page are in the InsideEntityTests.cs file in the DocCode teaching tests.
A domain model object represents something significant in the application domain. A “Customer”, for example, has data properties (“Name”), relationships to other entities (“Orders”) and perhaps some business logic (“IsGoldCustomer”). We bind these object members to UI controls and reason about them in application code. They are what matters most to users and other application stakeholders. They define “Customer-ness”.
The “Customer” is also an entity, a long-lived object with a permanent key. We can fetch it from a database, hold it in cache, check for changes, validate, and save it. When the developer’s attention turns to whether an object has changed or not, what its values used to be, how it is persisted, whether it has validation errors … the developer is thinking about the object’s entity nature. Breeze is responsible for the object’s entity nature, its “entity-ness”. You access an entity’s entity nature through its EntityType and EntityAspect properties.
Every Breeze entity instance has an EntityAspect
property that in turn has an EntityType
property that returns an EntityType object which is the metadata that describe its properties and other facts about the type.
var type = someCustomer.EntityAspect.EntityType;
A Breeze entity is “self-tracking”. It maintains its own entity state, and the means to change that state, in the EntityAspect object returned by its EntityAspect property.
An object becomes a Breeze entity when it acquires its EntityAspect which it does when it
first enters the cache as a result of a query or import OR
is created with the EntityType.CreateEntity factory method OR
is explicitly added or attached to an EntityManager
The first of any of these actions is sufficient to endow an object with its EntityAspect which it retains throughout its client session lifetime.
We’ll tackle EntityAspect’s key features in four groups.
EntityState … and the methods that can reset that state
PropertyChanged event
ValidateEntity … and related validation members
Is the entity attached to an EntityManager and therefore in its cache? Has it changed? If changed, is it a new entity, a modified version of an existing entity from remote storage, or an existing entity that is marked for deletion?
The EntityState property answers these questions with a value from the EntityState enumeration. Here are the enumeration names and their meanings:
EntityState | Meaning |
---|---|
Added | A new entity in cache that does not exist in the backend database. |
Unchanged | An existing entity in cache that was queried from the database; the entity has no unsaved changes since it was last retrieved or saved. |
Modified | An existing entity in cache with pending changes. |
Deleted | An existing entity in cache that is marked for deletion. |
Detached | An entity that is not in cache; its status in the database is unknown. |
You can test the value of an EntityState enumeration by comparing its name with a string. Or you may prefer to test with the enumeration’s properties and methods:
var state = anEntity.EntityAspect.EntityState;
if (state.ToString() == "Modified") {/* ... */}; // ok
if (state == EntityState.Modified) {/* ... */}; // better
if (state.IsModified()) {/* ... */}; // best
if (state.IsAddedOrModifiedOrDeleted()) {/* ... */}; // often useful
As things happen to an entity, Breeze updates its EntityState automatically. Here are before and after EntityStates for some of the most common actions:
Before | Action | After |
---|---|---|
Entity materialized in cache by a query | Unchanged | |
Unchanged | Set one of its properties | Modified |
Modified | Save it successfully | Unchanged |
Unchanged | Mark it deleted | Deleted |
Deleted | Save it | Detached |
Create a new entity | Detached | |
Detached | Add the new entity to the manager | Added |
Added | Delete it (or call RejectChanges | Detached |
Two state-changes may surprise you. If you mark an existing entity for deletion and save it successfully, the entity becomes detached. Breeze can’t make the entity disappear; it may still be visible in the UI. But the entity no longer exists on the server so Breeze banishes it from its former EntityManager cache.
Deleting a new entity detaches it immediately. Breeze doesn’t wait for you to call SaveChanges which is pointless if you’re discarding data that have never been saved.
A detached entity does not belong to an EntityManager. It’s still an entity; it’s just not an entity in any cache.
A detached entity should not be used. Either attach it to an EntityManager or release all references to it … and let it be garbage collected.
A detached entity is unreliable. It still has data values and you can still set them. But its navigation properties are not dependable and other entity features may behave unexpectedly. You can’t tell by inspection whether a detached entity has corresponding data in remote storage.
New entities start as detached entities. You might have to create them where no EntityManager is available. More likely, you have to initialize some of the new entity’s values before you can add it to an EntityManager. For example, because all entities in cache must have unique keys, if the entity key is client-determined (as opposed to store-generated), you must set the key to a unique value before you can attach the entity to an EntityManager.
You should initialize a new entity and then immediately add it to an EntityManager … unless you have a very good reason to do otherwise.
Entities can become detached deliberately or as a side-effect of another action. The following actions detach an entity:
explicitly removing it from its EntityManager (manager.DetachEntity(anEntity)
)
clearing its EntityManager (manager.Clear()
)
deleting a new entity
deleting an existing entity and then saving it successfully.
Note that removing an entity from cache (detaching it) does not delete it. The data of a pre-existing detached entity remain in remote storage.
You can change the EntityState programmatically through one of the EntityAspect methods dedicated to that purpose.
Delete()
RejectChanges()
AcceptChanges()
Call Delete() to schedule an entity for deletion as discussed below.
Call RejectChanges() to cancel pending changes as discussed below.
Call AcceptChanges() to set an entity’s state to “Unchanged”.
You rarely call AcceptChanges in production code; production entities become “Unchanged” as a side-effect of application activity.
You are most likely to call this method while setting up fake entities for automated tests because you want to force these fakes into a particular test state. The AcceptChanges method also clears the OriginalValuesMap, erasing memory of prior values; you won’t be able to revert these entities to their original values.
Deleting an entity begins with an EntityState change. Call Delete() to mark an entity for deletion:
someEntity.EntityAspect.Delete(); // mark for deletion
Delete
does not destroy the object locally nor does it remove the entity from the database. The entity simply remains in cache in a “Deleted” state … as changed and added entities do … until you save. A successful save deletes the entity from the database and removes it from cache.
Once you’ve changed an entity, it stays in a changed state … even if you manually restore the original values:
var oldCompanyName = customer.CompanyName; // assume existing "Unchanged" entity
customer.CompanyName = "Something new"; // EntityState becomes "Modified"
customer.CompanyName = oldCompanyName; // EntityState is still "Modified
Call RejectChanges to cancel pending changes, revert properties to their prior values, and set the EntityState to “Unchanged”.
var oldCompanyName = customer.CompanyName; // assume existing "Unchanged" entity
customer.CompanyName = "Something new"; // EntityState becomes "Modified"
customer.EntityAspect.RejectChanges(); // EntityState restored to "Unchanged”
// customer.CompanyName == oldCompanyName
You can also call RejectChanges on the EntityManager to cancel and revert pending changes for every entity in cache.
manager.RejectChanges(); // revert all pending changes in cache
Breeze remembers the original property values when you change an existing entity. It stores these values in the EntityAspect’s OriginalValuesMap. The OriginalValuesMap is an empty object while the entity is in the “Unchanged” state. When you change an entity property for the first time, Breeze adds the pre-change value to the OriginalValuesMap, using the property name as the key. The keys are the names of the properties that have been changed since the entity was last queried or saved.
Here’s a method to get those keys:
public IEnumerable<string> GetOriginalValuesPropertyNames(IEntity entity)
{
var names = new List<string>();
foreach (var name in entity.EntityAspect.OriginalValuesMap)
{
names.Add(name.Key);
}
return names;
}
Breeze replaces EntityAspect.OriginalValuesMap with a new empty hash when any operation restores the entity to the “Unchanged” state. A successful save, RejectChanges and AcceptChanges all reset the OriginalValuesMap.
Breeze creates entities in in the manner appropriate for .Net and WPF. This means that the entity is shaped to match the needs of WPF binding. INotifyPropertyChanged, INotifyDataErrorInfo, and several other interfaces discovered and used by WPF are defined and implemented by all Breeze entities. To use these methods outside of standard data binding you will need to cast your entity to the appropriate interface. i.e.
((INotifyPropertyChanged) aCustomer).PropertyChanged += ...
((INotifyDataErrorInfo) aCustomer).ErrorsChanged += ...
Breeze also has PropertyChanged and a EntityPropertyChanged events on the EntityAspect. The EntityPropertyChanged event is exactly the same as that raised by casting the entity itself to INotifyPropertyChanged as shown above. The PropertyChanged on the EntityAspect, the other hand, is intended for use in watching for changes to EntityAspect specific properties, like EntityState, EntityKey and IsChanged.
aCustomer.EntityAspect.EntityPropertyChanged += /* same as above */
vs aCustomer.EntityAspect.PropertyChanged += /* sees changes to EntityState, EntityKey etc. */
You can listen for a change to any Breeze-tracked entity property with the following:
Breeze only monitors changes to properties identified in the metadata for this EntityType. These properties - mapped and unmapped - are the “Breeze-tracked entity properties” mentioned earlier. Breeze doesn’t track properties that you add with an entity initialization method (see Extending Entities) or that you patch into the entity later in its lifetime.
Breeze properties aren’t just observable. They can validate changes based on rules registered in metadata. Some of the validations are registered automatically based on information in the metadata. For example, a key property is automatically required. You can add your own custom validations as well. See the Validation topic for details.
In brief, Breeze evaluates validation rules at prescribed times. It can also validate on demand. Call the EntityAspect.Validate to validate the entire entity which means every property validation rule as well as every entity-level validation rule. You can validate a single property (all of its rules) by calling EntityAspect.ValidateProperty. Again, see the Validation topic for details.
A validation rule either passes or fails. If it passes, it returns null. If it fails, it returns a ValidationError describing the problem.
Every EntityAspect maintains a ValidationErrors
collection. The Breeze validation engine adds a new ValidationError instance to that collection when a validation rules fails and removes an old ValidationError instance when its associated validation rule passes.
This last category is a small menagerie of miscellaneous EntityAspect members
Entity - a backward reference to the entity that holds this EntityAspect.
EntityManager - the EntityManager to which this entity is attached … or was attached. It’s null if the entity is new and not yet added to a manager.
EntityKey - the entity’s EntityKey. A key is an object that uniquely identifies the entity in cache and in remote storage. The key is not a simple value. It’s an object that identifies the type of the entity and the value … or values … of the key; Breeze supports entities with composite keys.
LoadNavigationProperty you can download related entities, on demand, by calling LoadNavigationProperty as described in the Navigation Properties topic.
You typically access the breeze entity infrastructure through the EntityAspect property. Breeze also has a few protected methods that are available from within your entity. Two of the more useful of these are:
***GetValue
SetValue - a method that sets a property value
With GetValue and SetValue, you can write utilities to access the properties of any Breeze entity as strings. These methods are also available with the same signatures on the EntityAspect.
The SetValue method follows the same code path as a property accessor and will raise property change events, change the entity state, and trigger validation accordingly.