NOTE: This page is for Breeze running on .NET 4.x
Go here for .NET Core version
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.
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
.
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:
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.
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.
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
}
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
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”).
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.
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
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.
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.
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();
}
}
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.