Saving changes

Save changes to entities in cache by calling the EntityManager’s saveChanges method. This topic delves more deeply into the save process and how you control it.

This page is not ready for publication. It will cover:

  • Detecting changes in cache: hasChanges and getChanges
  • Canceling pending changes with rejectChanges
  • Validation before save
  • The state of entities after save
  • What entities are in saveResult.entities collection.
  • Temporary key resolution (‘id fix-up’) and the saveResult.keyMappings.
  • Concurrency and the DataProperty.concurrencyMode
  • Saving selected entities
  • Default and explicit SaveOptions
  • Guard against accidental double saves
  • Saving a change-set to a specific server endpoint with a ‘named save
  • Saving data to an arbitrary HTTP service

Custom save operations with ‘named saves’

By default the EntityManager.saveChanges method sends a save request to a server endpoint called ‘SaveChanges’.

But you might have a specific business process to perform when you save a certain constellation of entities. Perhaps the actual storing of changes in the database is only a part of a much larger server-side workflow. What you really have is a ‘command’ that includes a database update.

You could route this command through a single ‘SaveChanges’ endpoint and let the corresponding server method dispatch the save request to the appropriate command handler. That could get messy. It can make more sense to POST requests to command-specific endpoints, passing along just the right entity set in the request body.

That’s what the ‘Named Save’ is for. With a ‘Named Save’, you can re-target a ‘save’ to a custom server endpoint such as an arbitrarily named action method on a separate, dedicated Web API controller.

You still call EntityManager.saveChangesbut you pass in a SaveOptions object that specifies the resourceName to handle the request. The server should route the request to a suitable controller action method. You’d also set the SaveOptions.dataService if you need also to target a different controller.

Assuming that you want to save all pending changes to a custom endpoint, you could write:

var so = new SaveOptions({ resourceName: 'myCustomSave' });
// null = 'all-pending-changes'; saveOptions is the 2nd parameter
myEntityManager.SaveChanges(null, so ); 

You are more likely to assemble a list of entities to save to that endpoint … a list consistent with the semantics of ‘MyCustomSave’ in which case you’d probably pass that list in the ‘saveChanges’ call:

myEntityManager.SaveChanges(selectedEntities, so ); 

The Breeze client still sends a JSON change-set bundle to ‘MyCustomSave’ as it would with a normal saveChanges call. The POST method on the server that handles the ‘MyCustomSave’ endpoint should have the same as signature as the ‘SaveChanges’ method.

The Save Bundle

Here is what a typical JSON payload looks like when SaveChanges is called (when using the default (web api) DataServiceAdapter):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
    {
        "entities": [
            {
                "OrderID": -1,
                "CustomerID": "785efa04-cbf2-4dd7-a7de-083ee17b6ad2",
                "EmployeeID": 1,
                "OrderDate": null,
                "ShippedDate": null,
                "Freight": null,
                "RowVersion": 0,
                "entityAspect": {
                    "entityTypeName": "Order:#Foo",
                    "defaultResourceName": "Orders",
                    "entityState": "Added",
                    "originalValuesMap": {
                    
                    },
                    "autoGeneratedKey": {
                    "propertyName": "OrderID",
                    "autoGeneratedKeyType": "Identity"
                    }
                }
            },
            {
                "OrderID": -1,
                "ProductID": 1,
                "UnitPrice": 0,
                "Quantity": 5,
                "Discount": 0,
                "RowVersion": 0,
                "entityAspect": {
                    "entityTypeName": "OrderDetail:#Foo",
                    "defaultResourceName": "OrderDetails",
                    "entityState": "Added",
                    "originalValuesMap": {
                    
                    },
                    "autoGeneratedKey": null
                }
            }
        ],
        "saveOptions": {
            
        }
    }

A few things to notice:

  • The entities are in a flat array. They are not connected in a graph by their navigation properties as they are when they are in the EntityManager. For example, the Order in the payload does not have an OrderDetails collection, and the OrderDetail does not have an Order property. The foreign keys (OrderID) are still populated, and these can be used on the server to identify the relationship.
  • Each entity has an entityAspect property, which identifies its entityTypeName and entityState. These are used to create the proper type on the server, and determine how to update the persistence layer
  • In this example, each entityState is Added, and the OrderID property has a temporary value that will be replaced by the server-generated id.
  • The originalValuesMap contains the original values of each property that was changed on the client. This provides a clue to the server about which server-side properties need to be updated.
  • The autoGeneratedKey property identifies the key generation policy of the entity according to the metadata. It provides a hint to the server about whether the client expects the server to provide a generated key if the entity is Added.
  • The saveOptions property is an object that can contain anything the client wants to send. This could be a clue to the server for special types of saves

The Save Response

Here is what a typical JSON response looks like when SaveChanges is called (when using a web api server):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
    {
        "$id": "1",
        "$type": "Breeze.ContextProvider.SaveResult, Breeze.ContextProvider",
        "Entities": [
            {
                "$id": "2",
                "$type": "Foo.Order, Model_NorthwindIB_CF.EF6",
                "OrderID": 212008,
                "CustomerID": "785efa04-cbf2-4dd7-a7de-083ee17b6ad2",
                "EmployeeID": 1,
                "OrderDate": null,
                "RequiredDate": null,
                "ShippedDate": null,
                "Freight": null,
                "ShipName": "Test 2016-07-08T17:50:10.982Z",
                "RowVersion": 0,
                "Customer": null,
                "Employee": null,
                "OrderDetails": [
                    {
                    "$id": "3",
                    "$type": "Foo.OrderDetail, Model_NorthwindIB_CF.EF6",
                    "OrderID": 212008,
                    "ProductID": 1,
                    "UnitPrice": 0,
                    "Quantity": 5,
                    "Discount": 0,
                    "RowVersion": 0,
                    "Order": {
                        "$ref": "2"
                    },
                    "Product": null
                    }
                ],
                "InternationalOrder": null
            },
            {
                "$ref": "3"
            }
        ],
        "KeyMappings": [
            {
                "$id": "5",
                "$type": "Breeze.ContextProvider.KeyMapping, Breeze.ContextProvider",
                "EntityTypeName": "Foo.Order",
                "TempValue": -1,
                "RealValue": 212008
            }
        ],
        "DeletedKeys": [
            {
                "$id": "6",
                "$type": "Breeze.ContextProvider.EntityKey, Breeze.ContextProvider",
                "EntityTypeName": "Foo.Supplier",
                "KeyValue": [
                    1210
                ]
            }
         ],
        "Errors": null
    }

Here we notice:

  • The entities may be connected in a graph, as they would be in a query response. This is not required, as long as they can be re-connected on the client by their foreign key properties.
  • The $id and $ref properties are used to identify the relationships between the entities in this payload only. The real entity relationship is established (on server and client) by the OrderID.
  • The temporary OrderID value has been replaced by the server-generated ID. The KeyMappings property identifies the keys that were replaced on the server. This tells the Breeze client how to update the entities in its cache. It will find the Order entity with OrderID -1, and change its OrderID to 212008, and change the foreign key in all related entities.
  • The DeletedKeys property contains the EntityKey values for entities that were deleted on the server but not the client. This tells the Breeze client to remove these entities from the cache.
  • Any errors (such as server-side validation errors) appear in the Errors property