PersistenceManager

NOTE: This page is for Breeze running on .NET Core
Go here for .NET 4.x version

The PersistenceManager is a server-side component for managing data access and business validation with .NET technologies.

PersistenceManager is the base class for the EFPersistenceManager and the NHPersistenceManager classes which rely on an ORM (EntityFramework and NHibernate respectively) for relational database access and metadata generation.

This topic covers the uses and capabilities of the PersistenceManager. While often described in connection with the EFPersistenceManager, please remember that it is more general than that.

You can use PersistenceManager as the basis for transforming BreezeJS query and save requests into actions performed against any kind of data store.

SaveChanges

Most of the PersistenceManager is devoted to saving client changes via the SaveChanges method.

The Breeze client (described elsewhere) automatically keeps track of any entities that have been added, modified or deleted and for any modifications exactly what was modified. When a Breeze client saveChanges call is made, the client posts a bundle of these changes as a request to a web api controller which routes the request and its payload to a PersistenceManager.SaveChanges method.

The request payload, called the “saveBundle”, is a BreezeJS JSON object (a JSON.NET JObject) that describes an entity change-set. An “entity change-set” is an arbitrary collection of entities to be saved. Each entity in the change-set is paired with a save-operation - add, update, delete - to be performed on that entity.

You can also pass in an optional second parameter, a TransactionSettings object, to set the transaction scope of the entire save process.

SaveChanges orchestrates the save. Along the way it calls several virtual “interceptor” methods: BeforeSaveEntity, BeforeSaveEntities, and AfterSaveEntities.

The BeforeSave... methods are your opportunity to validate the entities-to-be-saved and potentially manipulate the change-set. You can cancel the save here if you don’t like what you see.

In AfterSaveEntities you have access to the entities after they’ve been saved successfully. New entities now have their store-generated keys. This is your opportunity to perform-post save operations and manipulate the saved entities in memory before they are returned to the caller.

You do not have to subclass a PersistenceManager to provide before- and after-save logic.

You can attach handlers to the corresponding delegate properties of a PersistenceManager instance: BeforeSaveEntityDelegate, BeforeSaveEntitiesDelegate and AfterSaveEntitiesDelegate. There is no difference in functionality. Choose the approach that suits your architectural style.

Each of these methods receives entity change information in the form of an EntityInfo.

EntityInfo

The SaveChanges method translates the incoming JSON change-set (aka “SaveBundle”) into a dictionary of EntityInfo objects called a “save map”. The dictionary is keyed by entity type; the entry itself is a list of EntityInfo objects of that particular type.

An EntityInfo describes the entity-to-be-saved and the save operation to be performed on it. Here is its annotated interface:

// A back reference to the concrete PersistenceManager that created it
PersistenceManager ContextProvider { get; internal set; }

// The values to save represented as an instance of a .NET class
// Created for you by the PersistenceManager
Object Entity { get; internal set; }

// Whether the entity is to be added, updated, or deleted
EntityState EntityState { get;  set; }

// The properties that were modified and their pre-change values
// See discussion below
Dictionary<String, Object> OriginalValuesMap { get;  set; }

// True if an update operation must update every property
// False (default) if the update operation can update just the changed properties
bool ForceUpdate { get; set; }

// Information about the entity's key at a point in time
// Useful mainly for entities being added which have store-generated keys
AutoGeneratedKey AutoGeneratedKey { get; set; }

// JSON property names and values that did not map to .NET model class properties.
Dictionary<String, Object> UnmappedValuesMap { get; internal set; }

The EntityInfo.Entity is an instance of the .NET entity class that corresponds to a BreezeJS client entity. This .NET class may be - and often is - an “entity class” in your ORM model.

It does not have to be an ORM class. It could be a DTO class that you will later map into a class in your business model via your implementation of BeforeSaveEntities.

The EntityInfo.Entity properties have been populated with values in the JSON save request payload. These are client-provided values, not the values of a record in the data store. It may have foreign key properties that were set with client values. Do not assume that the corresponding navigation properties return the association’s related entities. Most PersistenceManager implementations, including EFPersistenceManager, disable the “lazy loading” that would populate these navigation properties for two very good reasons:

  1. we do not want to incur the performance cost of unnecessarily querying the data store during a save.
  2. we do not want to confuse related entities retrieved from the data store with the the related entities that may be in the change-set in an added, modified or deleted state.

EntityState

The EntityInfo.EntityState is an enum that describes the current state of the entity in the change-set.

public enum EntityState { Detached = 1, Unchanged = 2, Added = 4, Deleted = 8, Modified = 16, }

The PersistenceManager infers the intended save operation from this EntityState. For example, values of an entity in the Modified state will update the already-existing record in the data store with the matching entity key.

OriginalValuesMap

An EntityInfo that describes an entity-to-be-updated has an OriginalValuesMap.

This OriginalValuesMap is a {key,value} dictionary identifying which properties have changed and their pre-change values.

The PersistenceManager can (and usually will) use this map to update only the fields of the corresponding entity record in the data store that are keys of the OriginalValuesMap. You should assume that if a property is not a key in the OriginalValuesMap, that field will not be updated.

Updating a property

It follows that, when updating an entity, if you change one of its properties on the server and that property was not changed on the client, you should also add the property name to the OriginalValuesMap.

Alternatively, you can force update of every field by setting EntityInfo.ForceUpdate = True;

For example, we could calculate an entity property on the server in a BeforeSaveEntity method:

  public bool BeforeSaveEntity(EntityInfo info)  {

    if (info.EntityState == EntityState.Modified && info.Entity is Customer) {
      var cust = (Customer) info.Entity;
      cust.MOL = meaningOfLife();

      // Add property to map so that PersistenceManager updates db
      // original values don't matter
      info.OriginalValuesMap["MOL"] = null;
    }
      // ... more stuff
  }

Many apps set audit fields on the server for entities that have them. You might set them this way:

  public bool BeforeSaveEntity(EntityInfo info)  {
    if (info.EntityState == EntityState.Modified && info.Entity is IAuditable) {
      var auditable = (IAuditable) info.Entity;
      auditable.Modified = DateTime.UtcNow;
      auditable.UserId   = CurrentUser.Id;

      // Add property to map so that PersistenceManager updates db
      // original values don't matter
      info.OriginalValuesMap["Modified"] = null;
      info.OriginalValuesMap["UserId"] = null;
    }
    // ... more stuff
  }
Pre-change values

The PersistenceManager itself ignores the pre-change values in the OriginalValuesMap.

except for the concurrency field values where the pre-change value is used for optimistic concurrency checking.

The pre-change values may be useful to you when pre-processing the EntityInfo.

Beware! The OriginalValuesMap was provided by the client as part of its “save changes” request. It is your responsibility to confirm that this user is allowed to save changes to these properties. Before you make use of the pre-change values you should verify that the stated pre-change values actually are the “original values”.

The PersistenceManager assumes that the client request is valid. You are responsible for data integrity and data security. The BeforeSaveEntity and BeforSaveEntities methods are where you should perform such logic. You should scrutinize everything in the change-set to the degree that your business requires.

The EntityInfo and its OriginalValuesMap tell you what the client said in its save request. You inspect, validate, and modify that request in your overrides of the PersistenceManager methods.

BeforeSaveEntity

BeforeSaveEntity is called once for each entity before it is saved.

protected virtual bool BeforeSaveEntity(EntityInfo entityInfo) {)

Use it to inspect, validate, and potentially modify individual entities.

If the method returns false then the entity will be excluded from the save. If the method throws an exception, the entire save is aborted and the exception is returned to the client.

The base implementation of this method returns true;. There is no need to call the base implementation when overriding it.

BeforeSaveEntity is fine for validating each entity in isolation. Use BeforeSaveEntities to validate entities in the context of the entire changes-set (AKA “saveMap”).

BeforeSaveEntities

After PersistenceManager.SaveChanges calls BeforeSaveEntity for each EntityInfo, it calls BeforeSaveEntities on the entire change-set.

protected virtual Dictionary<Type, List<EntityInfo>> BeforeSaveEntities(
                  Dictionary<Type, List<EntityInfo>> saveMap)

A change-set often describes changes (adds, updates, deletes) to several entities that are all related somehow, perhaps because they are part of the same entity graph (an order and its details) or because they are part of the same workflow (“create new customer”). Your server-side business logic may need to evaluate all of the entities, both individually and together, as a single cohesive business operation.

BeforeSaveEntities is the best way to evaluate the entire change-set. Many developers only write a BeforeSaveEntities and do not bother with a BeforeSaveEntity.

The BeforeSaveEntities method receives the change-set in the form of a dictionary of EntityInfo objects. The dictionary has one entry per entity type and each entry is a list of EntityInfo objects describing an entity-to-be-saved.

Your custom BeforeSaveEntities can inspect, validate, add, remove, and modify EntityInfo objects in the change-set dictionary before returning that dictionary to `SaveChanges. Throwing an exception aborts the save.

You may want to create a new entity to save with the other entities in your change-set. Make a new EntityInfo by calling the CreateEntityInfo method whose signature is:

CreateEntityInfo(Object entity, EntityState entityState = EntityState.Added) 

Then add it to the change-set dictionary.

The base implementation of this method simply returns the incoming dictionary unchanged. There is no need to call the base implementation in your override.

Save Authorization

In your application, you will need to verify whether the user is allowed to make the changes that they’ve requested in the change set.

In your BeforeSaveEntities or delegate method, you would check to see if the entities can be saved by the current user. If you find an entity that shouldn’t be saved, you can either remove it from the change set, or throw an error and abort the save:

  protected override Dictionary<Type, List<EntityInfo>> BeforeSaveEntities(Dictionary<Type, List<EntityInfo>> saveMap) {
    var user = GetCurrentUser();  // get user from session
    foreach (Type type in saveMap.Keys)  {
      foreach (EntityInfo entityInfo in saveMap[type]) {
        if (!UserCanSave(entityInfo, user)) { // implement business rules
          throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Forbidden)
              { ReasonPhrase = "Not authorized to make these changes" });
        }
      }
    }
    return saveMap;
  }

You will need to determine whether the user should be allowed to save a particular entity. This could be based on the role of the user and/or some other attribute, e.g. users in the Sales role can only save Client records that belong to their own SalesRegion.

AfterSaveEntities

AfterSaveEntities gives access the to entities after they’ve been saved to the database, and after database-assigned identifiers have been assigned.

protected override void AfterSaveEntities(Dictionary<Type, List<EntityInfo>> saveMap, List<KeyMapping> keyMappings)

The saveMap parameter provides access to the full set of entities that were saved.

The keyMappings parameter provides the mapping of temporary IDs to real, db-assigned IDs.

Note that saveMap and keyMappings are the source of the data that forms the SaveResult that is sent back to the Breeze client. Any changes that you make to saveMap or keyMappings will affect Breeze’s ability to update the client with the correct data after the save.

SaveChangesCore

The SaveChangesCore method performs the save operations on the change-set. The PersistenceManager has no implementation of its own. It’s an abstract method to be implemented in a derived class.

Most developers rely on a pre-existing derived class such as the EFPersistenceManager to implement this method and there is rarely a need to override that implementation.

You will override it if you write your own PersistenceManager.

Wrapping the entire save process in a transaction

Most PersistenceManager implementations wrap the inner save processing within a transaction. The EFPersistenceManager does that.

But the actions of the BeforeSave... and AfterSaveEntities methods fall outside the boundaries of this particular transaction.

If you need to include BeforeSave... and AfterSaveEntities processing within the save transaction, you must supply the optional TransactionSettings parameter to the SaveChanges call.

Here’s an example:

  [HttpPost]
  public SaveResult SaveWithTransactionScope(JObject saveBundle) {
    var txSettings = new TransactionSettings() { TransactionType = TransactionType.TransactionScope };
  
      // Add the specialized AfterSave handler
    PersistenceManager.AfterSaveEntitiesDelegate = PerformPostSaveValidation;
  
    return PersistenceManager.SaveChanges(saveBundle, txSettings);
  }

  private void PerformPostSaveValidation(Dictionary<Type, List<EntityInfo>> saveMap, List<KeyMapping> keyMappings ) {
    // do your post save validation stuff here
    // and throw an exception if something doesn't validate.
  
  }

Now the entire save process occurs within a TransactionScope including the BeforeSaveEntities and the AfterSaveEntities invocations. Now you can abort the transaction after saving to the database by throwing an exception in your AfterSaveEntities method; doing so will rollback all previous inserts, updates or deletes that were part of the transaction.

This discussion presupposes that the technologies involved support transactions.

You may want to call this particular SaveWithTransactionScope method only for certain client requests. You can add a dedicated endpoint for that purpose to your Web API controller and call it from the client with a named save.

[HttpPost]
public SaveResult SpecialSave(JObject saveBundle) {
  return _repository.SaveWithTransactionScope(saveBundle);
}

TransactionSettings has the following properties:

  • TransactionType: Has one of the following values:

    • TransactionScope - use the .NET TransactionScope (recommended for SQL Server; required for distributed transactions)

    • DbTransaction - use the database native transactions (recommended for Oracle)

    • None (the default; provided for backward compatibility with prior Breeze releases)

  • IsolationLevel: Defines the transaction isolation, using the System.Transactions.IsolationLevel values. Defaults to ReadCommitted. When using TransactionType.DbTransaction, the supported levels depend upon the data provider.

  • Timeout: The timeout period for the transaction. Only applies to TransactionType.TransactionScope. Default is TransactionManager.DefaultTimeout, which can be set in web.config.