ContextProvider

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

ContextProvider is the base class for the EFContextProvider and the NHContextProvider 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 ContextProvider. While often described in connection with the EFContextProvider, please remember that it is more general than that.

You can use ContextProvider as the basis for transforming BreezeJS query and save requests into actions performed against any kind of data store. See, for example, the the in-memory "No DB" sample.

SaveChanges

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

Typically, a Breeze client posts a saveChanges request to a web api controller which routes the request and its payload to a ContextProvider.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 ContextProvider to provide before- and after-save logic.

You can attach handlers to the corresponding delegate properties of a ContextProvider 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 ContextProvider that created it
ContextProvider ContextProvider { get; internal set; }

// The values to save represented as an instance of a .NET class
// Created for you by the ContextProvider
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 ContextProvider implementations, including EFContextProvider, 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 ContextProvider 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 ContextProvider 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 ContextProvider 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 ContextProvider updates db
        // original values don't matter
        info.OriginalValuesMap["Modified"] = null;
        info.OriginalValuesMap["UserId"] = null;
    }
      // ... more stuff
 }
Pre-change values

The ContextProvider 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 ContextProvider assumes that the client request is valid. You are responsible for data integrity and data security. 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 ContextProvider 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 ContextProvider.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.

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 ContextProvider 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 EFContextProvider to implement this method and there is rarely a need to override that implementation.

You will override it if you write your own ContextProvider. That task is beyond the scope of this topic. You'll find clues in the TodoContext class of the "NoDB" sample and in the source code for the EFContextProvider. There's also a discussion of ContextProvider extensibility in this StackOverflow answer.

ContextProvider helper methods

The ContextProvider exposes public methods to help in the implementation of your virtual method overrides.

The following helpers enable re-use of database connections; such re-use reduces the need for distributed transactions:

GetDbConnection provides access to the underlying connection to the database. This is the same connection that ContextProvider uses to save the entity changes to the database. Re-using this same connection allows you to perform queries and updates without a separate connection which might cause a distributed transaction. The return value may be a EntityConnection, SqlConnection, etc. depending upon the specific ContextProvider implementation. For Entity Framework, use the EntityConnection and StoreConnection properties below.

EntityConnection is a read-only property that provides access to the EntityConnection used by the DbContext/ObjectContext. This is useful when you want to create a second DbContext with the same connection:

            protected override Dictionary<Type, List<EntityInfo>> BeforeSaveEntities(Dictionary<Type, List<EntityInfo>> saveMap) 
            {
                var context2 = new MyDbContext(EntityConnection);    // create a DbContext using the existing connection
                var orders = saveMap[typeof(Order)];    // get the EntityInfo for all Orders in the saveMap
                foreach(var orderInfo in orders)
                {
                    var order = orderInfo.Entity as Order;    // get the order that came from the client
                    var query = context2.Orders.Where(o => o.orderID == order.orderID);
                    var oldOrder = query.FirstOrDefault();    // get the existing order from the database
                    // compare values of order and oldOrder to see what has changed
                    // because we have a business rule that only certain changes are allowed
                    // ...
                }
            }

StoreConnection is a read-only property that provides access to the StoreConnection used by the DbContext/ObjectContext. This may be a SqlConnection, OracleConnection, etc. depending upon the provider. Use StoreConnection to do SQL queryies and updates directly to the database:

        protected override void AfterSaveEntities(Dictionary<Type, List<EntityInfo>> 
            saveMap, List<KeyMapping> keyMappings) 
        {
            // simplistic example of logging the created entity IDs
            var text = ("insert into AuditAddedEntities (CreatedOn, EntityType, EntityKey) values ('{0}', '{1}', {2})";
            var time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
            var conn = StoreConnection;    // use the existing StoreConnection
            var cmd = conn.CreateCommand();
            foreach (var km in keyMappings) 
            {
                // put the real value of the key into the audit table
                cmd.CommandText = String.Format(text, time, km.EntityTypeName, km.RealValue);
                cmd.ExecuteNonQuery();
            }
        }

Wrapping the entire save process in a transaction

Most ContextProvider implementations wrap the inner save processing within a transaction. The EFContextProvider 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:

public SaveResult SaveWithTransactionScope(JObject saveBundle) {
  var txSettings = new TransactionSettings() { TransactionType = TransactionType.TransactionScope };

  // Add the specialized AfterSave handler
  ContextProvider.AfterSaveEntitiesDelegate = PerformPostSaveValidation;

  return ContextProvider.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.